зеркало из https://github.com/mozilla/gecko-dev.git
1431 строка
48 KiB
C++
1431 строка
48 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
|
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "mozilla/dom/KeyframeEffectReadOnly.h"
|
|
|
|
#include "mozilla/dom/KeyframeAnimationOptionsBinding.h"
|
|
// For UnrestrictedDoubleOrKeyframeAnimationOptions;
|
|
#include "mozilla/dom/CSSPseudoElement.h"
|
|
#include "mozilla/dom/KeyframeEffectBinding.h"
|
|
#include "mozilla/AnimationUtils.h"
|
|
#include "mozilla/EffectSet.h"
|
|
#include "mozilla/FloatingPoint.h" // For IsFinite
|
|
#include "mozilla/LookAndFeel.h" // For LookAndFeel::GetInt
|
|
#include "mozilla/KeyframeUtils.h"
|
|
#include "mozilla/ServoBindings.h"
|
|
#include "mozilla/StyleAnimationValue.h"
|
|
#include "Layers.h" // For Layer
|
|
#include "nsComputedDOMStyle.h" // nsComputedDOMStyle::GetStyleContextForElement
|
|
#include "nsContentUtils.h" // nsContentUtils::ReportToConsole
|
|
#include "nsCSSPropertyIDSet.h"
|
|
#include "nsCSSProps.h" // For nsCSSProps::PropHasFlags
|
|
#include "nsCSSPseudoElements.h" // For CSSPseudoElementType
|
|
#include "nsIPresShell.h"
|
|
#include "nsIScriptError.h"
|
|
|
|
namespace mozilla {
|
|
|
|
bool
|
|
PropertyValuePair::operator==(const PropertyValuePair& aOther) const
|
|
{
|
|
if (mProperty != aOther.mProperty || mValue != aOther.mValue) {
|
|
return false;
|
|
}
|
|
if (mServoDeclarationBlock == aOther.mServoDeclarationBlock) {
|
|
return true;
|
|
}
|
|
if (!mServoDeclarationBlock || !aOther.mServoDeclarationBlock) {
|
|
return false;
|
|
}
|
|
return Servo_DeclarationBlock_Equals(mServoDeclarationBlock,
|
|
aOther.mServoDeclarationBlock);
|
|
}
|
|
|
|
namespace dom {
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly,
|
|
AnimationEffectReadOnly,
|
|
mTarget)
|
|
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(KeyframeEffectReadOnly,
|
|
AnimationEffectReadOnly)
|
|
NS_IMPL_CYCLE_COLLECTION_TRACE_END
|
|
|
|
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(KeyframeEffectReadOnly)
|
|
NS_INTERFACE_MAP_END_INHERITING(AnimationEffectReadOnly)
|
|
|
|
NS_IMPL_ADDREF_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
|
|
NS_IMPL_RELEASE_INHERITED(KeyframeEffectReadOnly, AnimationEffectReadOnly)
|
|
|
|
KeyframeEffectReadOnly::KeyframeEffectReadOnly(
|
|
nsIDocument* aDocument,
|
|
const Maybe<OwningAnimationTarget>& aTarget,
|
|
const TimingParams& aTiming,
|
|
const KeyframeEffectParams& aOptions)
|
|
: KeyframeEffectReadOnly(aDocument, aTarget,
|
|
new AnimationEffectTimingReadOnly(aDocument,
|
|
aTiming),
|
|
aOptions)
|
|
{
|
|
}
|
|
|
|
KeyframeEffectReadOnly::KeyframeEffectReadOnly(
|
|
nsIDocument* aDocument,
|
|
const Maybe<OwningAnimationTarget>& aTarget,
|
|
AnimationEffectTimingReadOnly* aTiming,
|
|
const KeyframeEffectParams& aOptions)
|
|
: AnimationEffectReadOnly(aDocument, aTiming)
|
|
, mTarget(aTarget)
|
|
, mEffectOptions(aOptions)
|
|
, mInEffectOnLastAnimationTimingUpdate(false)
|
|
, mCumulativeChangeHint(nsChangeHint(0))
|
|
{
|
|
}
|
|
|
|
JSObject*
|
|
KeyframeEffectReadOnly::WrapObject(JSContext* aCx,
|
|
JS::Handle<JSObject*> aGivenProto)
|
|
{
|
|
return KeyframeEffectReadOnlyBinding::Wrap(aCx, this, aGivenProto);
|
|
}
|
|
|
|
IterationCompositeOperation
|
|
KeyframeEffectReadOnly::IterationComposite() const
|
|
{
|
|
return mEffectOptions.mIterationComposite;
|
|
}
|
|
|
|
CompositeOperation
|
|
KeyframeEffectReadOnly::Composite() const
|
|
{
|
|
return CompositeOperation::Replace;
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
// 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) {
|
|
MarkCascadeNeedsUpdate();
|
|
mInEffectOnLastAnimationTimingUpdate = inEffect;
|
|
}
|
|
|
|
// Request restyle if necessary.
|
|
if (mAnimation && !mProperties.IsEmpty() && HasComputedTimingChanged()) {
|
|
EffectCompositor::RestyleType restyleType =
|
|
CanThrottle() ?
|
|
EffectCompositor::RestyleType::Throttled :
|
|
EffectCompositor::RestyleType::Standard;
|
|
RequestRestyle(restyleType);
|
|
}
|
|
|
|
// If we're no longer "in effect", our ComposeStyle method will never be
|
|
// called and we will never have a chance to update mProgressOnLastCompose
|
|
// and mCurrentIterationOnLastCompose.
|
|
// We clear them here to ensure that if we later become "in effect" we will
|
|
// request a restyle (above).
|
|
if (!inEffect) {
|
|
mProgressOnLastCompose.SetNull();
|
|
mCurrentIterationOnLastCompose = 0;
|
|
}
|
|
}
|
|
|
|
static bool
|
|
KeyframesEqualIgnoringComputedOffsets(const nsTArray<Keyframe>& aLhs,
|
|
const nsTArray<Keyframe>& aRhs)
|
|
{
|
|
if (aLhs.Length() != aRhs.Length()) {
|
|
return false;
|
|
}
|
|
|
|
for (size_t i = 0, len = aLhs.Length(); i < len; ++i) {
|
|
const Keyframe& a = aLhs[i];
|
|
const Keyframe& b = aRhs[i];
|
|
if (a.mOffset != b.mOffset ||
|
|
a.mTimingFunction != b.mTimingFunction ||
|
|
a.mPropertyValues != b.mPropertyValues) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// https://w3c.github.io/web-animations/#dom-keyframeeffect-setkeyframes
|
|
void
|
|
KeyframeEffectReadOnly::SetKeyframes(JSContext* aContext,
|
|
JS::Handle<JSObject*> aKeyframes,
|
|
ErrorResult& aRv)
|
|
{
|
|
nsTArray<Keyframe> keyframes =
|
|
KeyframeUtils::GetKeyframesFromObject(aContext, mDocument, aKeyframes, aRv);
|
|
if (aRv.Failed()) {
|
|
return;
|
|
}
|
|
|
|
RefPtr<nsStyleContext> styleContext = GetTargetStyleContext();
|
|
SetKeyframes(Move(keyframes), styleContext);
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::SetKeyframes(nsTArray<Keyframe>&& aKeyframes,
|
|
nsStyleContext* aStyleContext)
|
|
{
|
|
if (KeyframesEqualIgnoringComputedOffsets(aKeyframes, mKeyframes)) {
|
|
return;
|
|
}
|
|
|
|
mKeyframes = Move(aKeyframes);
|
|
// Apply distribute spacing irrespective of the spacing mode. We will apply
|
|
// the specified spacing mode when we generate computed animation property
|
|
// values from the keyframes since both operations require a style context
|
|
// and need to be performed whenever the style context changes.
|
|
KeyframeUtils::ApplyDistributeSpacing(mKeyframes);
|
|
|
|
if (mAnimation && mAnimation->IsRelevant()) {
|
|
nsNodeUtils::AnimationChanged(mAnimation);
|
|
}
|
|
|
|
if (aStyleContext) {
|
|
UpdateProperties(aStyleContext);
|
|
MaybeUpdateFrameForCompositor();
|
|
}
|
|
}
|
|
|
|
const AnimationProperty*
|
|
KeyframeEffectReadOnly::GetEffectiveAnimationOfProperty(
|
|
nsCSSPropertyID aProperty) const
|
|
{
|
|
EffectSet* effectSet =
|
|
EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
|
|
for (size_t propIdx = 0, propEnd = mProperties.Length();
|
|
propIdx != propEnd; ++propIdx) {
|
|
if (aProperty == mProperties[propIdx].mProperty) {
|
|
const AnimationProperty* result = &mProperties[propIdx];
|
|
// Skip if there is a property of animation level that is overridden
|
|
// by !important rules.
|
|
if (effectSet &&
|
|
effectSet->PropertiesWithImportantRules()
|
|
.HasProperty(result->mProperty) &&
|
|
effectSet->PropertiesForAnimationsLevel()
|
|
.HasProperty(result->mProperty)) {
|
|
result = nullptr;
|
|
}
|
|
return result;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
bool
|
|
KeyframeEffectReadOnly::HasAnimationOfProperty(nsCSSPropertyID aProperty) const
|
|
{
|
|
for (const AnimationProperty& property : mProperties) {
|
|
if (property.mProperty == aProperty) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
bool
|
|
SpecifiedKeyframeArraysAreEqual(const nsTArray<Keyframe>& aA,
|
|
const nsTArray<Keyframe>& aB)
|
|
{
|
|
if (aA.Length() != aB.Length()) {
|
|
return false;
|
|
}
|
|
|
|
for (size_t i = 0; i < aA.Length(); i++) {
|
|
const Keyframe& a = aA[i];
|
|
const Keyframe& b = aB[i];
|
|
if (a.mOffset != b.mOffset ||
|
|
a.mTimingFunction != b.mTimingFunction ||
|
|
a.mPropertyValues != b.mPropertyValues) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
void
|
|
KeyframeEffectReadOnly::UpdateProperties(nsStyleContext* aStyleContext)
|
|
{
|
|
MOZ_ASSERT(aStyleContext);
|
|
|
|
nsTArray<AnimationProperty> properties = BuildProperties(aStyleContext);
|
|
|
|
if (mProperties == properties) {
|
|
return;
|
|
}
|
|
|
|
// Preserve the state of the mIsRunningOnCompositor flag.
|
|
nsCSSPropertyIDSet runningOnCompositorProperties;
|
|
|
|
for (const AnimationProperty& property : mProperties) {
|
|
if (property.mIsRunningOnCompositor) {
|
|
runningOnCompositorProperties.AddProperty(property.mProperty);
|
|
}
|
|
}
|
|
|
|
mProperties = Move(properties);
|
|
|
|
for (AnimationProperty& property : mProperties) {
|
|
property.mIsRunningOnCompositor =
|
|
runningOnCompositorProperties.HasProperty(property.mProperty);
|
|
}
|
|
|
|
// FIXME (bug 1303235): Do this for Servo too
|
|
if (aStyleContext->PresContext()->StyleSet()->IsGecko()) {
|
|
CalculateCumulativeChangeHint(aStyleContext);
|
|
}
|
|
|
|
MarkCascadeNeedsUpdate();
|
|
|
|
RequestRestyle(EffectCompositor::RestyleType::Layer);
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::ComposeStyle(
|
|
RefPtr<AnimValuesStyleRule>& aStyleRule,
|
|
const nsCSSPropertyIDSet& aPropertiesToSkip)
|
|
{
|
|
ComputedTiming computedTiming = GetComputedTiming();
|
|
mProgressOnLastCompose = computedTiming.mProgress;
|
|
mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
|
|
|
|
// If the progress is null, we don't have fill data for the current
|
|
// time so we shouldn't animate.
|
|
if (computedTiming.mProgress.IsNull()) {
|
|
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 (aPropertiesToSkip.HasProperty(prop.mProperty)) {
|
|
continue;
|
|
}
|
|
|
|
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();
|
|
while (segment->mToKey <= computedTiming.mProgress.Value()) {
|
|
MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
|
|
if ((segment+1) == segmentEnd) {
|
|
break;
|
|
}
|
|
++segment;
|
|
MOZ_ASSERT(segment->mFromKey == (segment-1)->mToKey, "incorrect keys");
|
|
}
|
|
MOZ_ASSERT(segment->mFromKey <= segment->mToKey, "incorrect keys");
|
|
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.
|
|
aStyleRule = new AnimValuesStyleRule();
|
|
}
|
|
|
|
StyleAnimationValue fromValue = segment->mFromValue;
|
|
StyleAnimationValue toValue = segment->mToValue;
|
|
// Iteration composition for accumulate
|
|
if (mEffectOptions.mIterationComposite ==
|
|
IterationCompositeOperation::Accumulate &&
|
|
computedTiming.mCurrentIteration > 0) {
|
|
const AnimationPropertySegment& lastSegment =
|
|
prop.mSegments.LastElement();
|
|
// FIXME: Bug 1293492: Add a utility function to calculate both of
|
|
// below StyleAnimationValues.
|
|
DebugOnly<bool> accumulateResult =
|
|
StyleAnimationValue::Accumulate(prop.mProperty,
|
|
fromValue,
|
|
lastSegment.mToValue,
|
|
computedTiming.mCurrentIteration);
|
|
// We can't check the accumulation result in case of filter property.
|
|
// That's because some filter property can't accumulate,
|
|
// e.g. 'contrast(2) brightness(2)' onto 'brightness(1) contrast(1)'
|
|
// because of mismatch of the order.
|
|
MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter,
|
|
"could not accumulate value");
|
|
accumulateResult =
|
|
StyleAnimationValue::Accumulate(prop.mProperty,
|
|
toValue,
|
|
lastSegment.mToValue,
|
|
computedTiming.mCurrentIteration);
|
|
MOZ_ASSERT(accumulateResult || prop.mProperty == eCSSProperty_filter,
|
|
"could not accumulate value");
|
|
}
|
|
|
|
// Special handling for zero-length segments
|
|
if (segment->mToKey == segment->mFromKey) {
|
|
if (computedTiming.mProgress.Value() < 0) {
|
|
aStyleRule->AddValue(prop.mProperty, Move(fromValue));
|
|
} else {
|
|
aStyleRule->AddValue(prop.mProperty, Move(toValue));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
double positionInSegment =
|
|
(computedTiming.mProgress.Value() - segment->mFromKey) /
|
|
(segment->mToKey - segment->mFromKey);
|
|
double valuePosition =
|
|
ComputedTimingFunction::GetPortion(segment->mTimingFunction,
|
|
positionInSegment,
|
|
computedTiming.mBeforeFlag);
|
|
|
|
MOZ_ASSERT(IsFinite(valuePosition), "Position value should be finite");
|
|
StyleAnimationValue val;
|
|
if (StyleAnimationValue::Interpolate(prop.mProperty,
|
|
fromValue,
|
|
toValue,
|
|
valuePosition, val)) {
|
|
aStyleRule->AddValue(prop.mProperty, Move(val));
|
|
} else if (valuePosition < 0.5) {
|
|
aStyleRule->AddValue(prop.mProperty, Move(fromValue));
|
|
} else {
|
|
aStyleRule->AddValue(prop.mProperty, Move(toValue));
|
|
}
|
|
}
|
|
}
|
|
|
|
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.
|
|
for (const AnimationProperty& property : mProperties) {
|
|
if (property.mIsRunningOnCompositor) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::SetIsRunningOnCompositor(nsCSSPropertyID 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");
|
|
for (AnimationProperty& property : mProperties) {
|
|
if (property.mProperty == aProperty) {
|
|
property.mIsRunningOnCompositor = aIsRunning;
|
|
// 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();
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::ResetIsRunningOnCompositor()
|
|
{
|
|
for (AnimationProperty& property : mProperties) {
|
|
property.mIsRunningOnCompositor = false;
|
|
}
|
|
}
|
|
|
|
static const KeyframeEffectOptions&
|
|
KeyframeEffectOptionsFromUnion(
|
|
const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions)
|
|
{
|
|
MOZ_ASSERT(aOptions.IsKeyframeEffectOptions());
|
|
return aOptions.GetAsKeyframeEffectOptions();
|
|
}
|
|
|
|
static const KeyframeEffectOptions&
|
|
KeyframeEffectOptionsFromUnion(
|
|
const UnrestrictedDoubleOrKeyframeAnimationOptions& aOptions)
|
|
{
|
|
MOZ_ASSERT(aOptions.IsKeyframeAnimationOptions());
|
|
return aOptions.GetAsKeyframeAnimationOptions();
|
|
}
|
|
|
|
template <class OptionsType>
|
|
static KeyframeEffectParams
|
|
KeyframeEffectParamsFromUnion(const OptionsType& aOptions,
|
|
nsAString& aInvalidPacedProperty,
|
|
ErrorResult& aRv)
|
|
{
|
|
KeyframeEffectParams result;
|
|
if (!aOptions.IsUnrestrictedDouble()) {
|
|
const KeyframeEffectOptions& options =
|
|
KeyframeEffectOptionsFromUnion(aOptions);
|
|
KeyframeEffectParams::ParseSpacing(options.mSpacing,
|
|
result.mSpacingMode,
|
|
result.mPacedProperty,
|
|
aInvalidPacedProperty,
|
|
aRv);
|
|
// Ignore iterationComposite if the Web Animations API is not enabled,
|
|
// then the default value 'Replace' will be used.
|
|
if (AnimationUtils::IsCoreAPIEnabledForCaller()) {
|
|
result.mIterationComposite = options.mIterationComposite;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/* static */ Maybe<OwningAnimationTarget>
|
|
KeyframeEffectReadOnly::ConvertTarget(
|
|
const Nullable<ElementOrCSSPseudoElement>& aTarget)
|
|
{
|
|
// Return value optimization.
|
|
Maybe<OwningAnimationTarget> result;
|
|
|
|
if (aTarget.IsNull()) {
|
|
return result;
|
|
}
|
|
|
|
const ElementOrCSSPseudoElement& target = aTarget.Value();
|
|
MOZ_ASSERT(target.IsElement() || target.IsCSSPseudoElement(),
|
|
"Uninitialized target");
|
|
|
|
if (target.IsElement()) {
|
|
result.emplace(&target.GetAsElement());
|
|
} else {
|
|
RefPtr<Element> elem = target.GetAsCSSPseudoElement().ParentElement();
|
|
result.emplace(elem, target.GetAsCSSPseudoElement().GetType());
|
|
}
|
|
return result;
|
|
}
|
|
|
|
template <class KeyframeEffectType, class OptionsType>
|
|
/* static */ already_AddRefed<KeyframeEffectType>
|
|
KeyframeEffectReadOnly::ConstructKeyframeEffect(
|
|
const GlobalObject& aGlobal,
|
|
const Nullable<ElementOrCSSPseudoElement>& aTarget,
|
|
JS::Handle<JSObject*> aKeyframes,
|
|
const OptionsType& aOptions,
|
|
ErrorResult& aRv)
|
|
{
|
|
nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
|
|
if (!doc) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
TimingParams timingParams =
|
|
TimingParams::FromOptionsUnion(aOptions, doc, aRv);
|
|
if (aRv.Failed()) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsAutoString invalidPacedProperty;
|
|
KeyframeEffectParams effectOptions =
|
|
KeyframeEffectParamsFromUnion(aOptions, invalidPacedProperty, aRv);
|
|
if (aRv.Failed()) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (!invalidPacedProperty.IsEmpty()) {
|
|
const char16_t* params[] = { invalidPacedProperty.get() };
|
|
nsContentUtils::ReportToConsole(nsIScriptError::warningFlag,
|
|
NS_LITERAL_CSTRING("Animation"),
|
|
doc,
|
|
nsContentUtils::eDOM_PROPERTIES,
|
|
"UnanimatablePacedProperty",
|
|
params, ArrayLength(params));
|
|
}
|
|
|
|
Maybe<OwningAnimationTarget> target = ConvertTarget(aTarget);
|
|
RefPtr<KeyframeEffectType> effect =
|
|
new KeyframeEffectType(doc, target, timingParams, effectOptions);
|
|
|
|
effect->SetKeyframes(aGlobal.Context(), aKeyframes, aRv);
|
|
if (aRv.Failed()) {
|
|
return nullptr;
|
|
}
|
|
|
|
return effect.forget();
|
|
}
|
|
|
|
template<class KeyframeEffectType>
|
|
/* static */ already_AddRefed<KeyframeEffectType>
|
|
KeyframeEffectReadOnly::ConstructKeyframeEffect(const GlobalObject& aGlobal,
|
|
KeyframeEffectReadOnly& aSource,
|
|
ErrorResult& aRv)
|
|
{
|
|
nsIDocument* doc = AnimationUtils::GetCurrentRealmDocument(aGlobal.Context());
|
|
if (!doc) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return nullptr;
|
|
}
|
|
|
|
// Create a new KeyframeEffectReadOnly object with aSource's target,
|
|
// iteration composite operation, composite operation, and spacing mode.
|
|
// The constructor creates a new AnimationEffect(ReadOnly) object by
|
|
// aSource's TimingParams.
|
|
// Note: we don't need to re-throw exceptions since the value specified on
|
|
// aSource's timing object can be assumed valid.
|
|
RefPtr<KeyframeEffectType> effect =
|
|
new KeyframeEffectType(doc,
|
|
aSource.mTarget,
|
|
aSource.SpecifiedTiming(),
|
|
aSource.mEffectOptions);
|
|
// Copy cumulative change hint. mCumulativeChangeHint should be the same as
|
|
// the source one because both of targets are the same.
|
|
effect->mCumulativeChangeHint = aSource.mCumulativeChangeHint;
|
|
|
|
// Copy aSource's keyframes and animation properties.
|
|
// Note: We don't call SetKeyframes directly, which might revise the
|
|
// computed offsets and rebuild the animation properties.
|
|
// FIXME: Bug 1314537: We have to make sure SharedKeyframeList is handled
|
|
// properly.
|
|
effect->mKeyframes = aSource.mKeyframes;
|
|
effect->mProperties = aSource.mProperties;
|
|
return effect.forget();
|
|
}
|
|
|
|
nsTArray<AnimationProperty>
|
|
KeyframeEffectReadOnly::BuildProperties(nsStyleContext* aStyleContext)
|
|
{
|
|
MOZ_ASSERT(aStyleContext);
|
|
|
|
nsTArray<AnimationProperty> result;
|
|
// If mTarget is null, return an empty property array.
|
|
if (!mTarget) {
|
|
return result;
|
|
}
|
|
|
|
// When GetComputedKeyframeValues or GetAnimationPropertiesFromKeyframes
|
|
// calculate computed values from |mKeyframes|, they could possibly
|
|
// trigger a subsequent restyle in which we rebuild animations. If that
|
|
// happens we could find that |mKeyframes| is overwritten while it is
|
|
// being iterated over. Normally that shouldn't happen but just in case we
|
|
// make a copy of |mKeyframes| first and iterate over that instead.
|
|
auto keyframesCopy(mKeyframes);
|
|
|
|
nsTArray<ComputedKeyframeValues> computedValues =
|
|
KeyframeUtils::GetComputedKeyframeValues(keyframesCopy,
|
|
mTarget->mElement,
|
|
aStyleContext);
|
|
|
|
if (mEffectOptions.mSpacingMode == SpacingMode::paced) {
|
|
KeyframeUtils::ApplySpacing(keyframesCopy, SpacingMode::paced,
|
|
mEffectOptions.mPacedProperty,
|
|
computedValues, aStyleContext);
|
|
}
|
|
|
|
result = KeyframeUtils::GetAnimationPropertiesFromKeyframes(keyframesCopy,
|
|
computedValues,
|
|
aStyleContext);
|
|
|
|
#ifdef DEBUG
|
|
MOZ_ASSERT(SpecifiedKeyframeArraysAreEqual(mKeyframes, keyframesCopy),
|
|
"Apart from the computed offset members, the keyframes array"
|
|
" should not be modified");
|
|
#endif
|
|
|
|
mKeyframes.SwapElements(keyframesCopy);
|
|
return result;
|
|
}
|
|
|
|
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->mElement, mTarget->mPseudoType);
|
|
effectSet->AddEffect(*this);
|
|
} else {
|
|
UnregisterTarget();
|
|
}
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::UnregisterTarget()
|
|
{
|
|
EffectSet* effectSet =
|
|
EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
|
|
if (effectSet) {
|
|
effectSet->RemoveEffect(*this);
|
|
if (effectSet->IsEmpty()) {
|
|
EffectSet::DestroyEffectSet(mTarget->mElement, mTarget->mPseudoType);
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::RequestRestyle(
|
|
EffectCompositor::RestyleType aRestyleType)
|
|
{
|
|
nsPresContext* presContext = GetPresContext();
|
|
if (presContext && mTarget && mAnimation) {
|
|
presContext->EffectCompositor()->
|
|
RequestRestyle(mTarget->mElement, mTarget->mPseudoType,
|
|
aRestyleType, mAnimation->CascadeLevel());
|
|
}
|
|
}
|
|
|
|
already_AddRefed<nsStyleContext>
|
|
KeyframeEffectReadOnly::GetTargetStyleContext()
|
|
{
|
|
nsIPresShell* shell = GetPresShell();
|
|
if (!shell) {
|
|
return nullptr;
|
|
}
|
|
|
|
MOZ_ASSERT(mTarget,
|
|
"Should only have a presshell when we have a target element");
|
|
|
|
nsIAtom* pseudo = mTarget->mPseudoType < CSSPseudoElementType::Count
|
|
? nsCSSPseudoElements::GetPseudoAtom(mTarget->mPseudoType)
|
|
: nullptr;
|
|
return nsComputedDOMStyle::GetStyleContextForElement(mTarget->mElement,
|
|
pseudo, shell);
|
|
}
|
|
|
|
#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;
|
|
Unused << StyleAnimationValue::UncomputeValue(p.mProperty,
|
|
s.mFromValue,
|
|
fromValue);
|
|
Unused << 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
|
|
|
|
/* static */ already_AddRefed<KeyframeEffectReadOnly>
|
|
KeyframeEffectReadOnly::Constructor(
|
|
const GlobalObject& aGlobal,
|
|
const Nullable<ElementOrCSSPseudoElement>& aTarget,
|
|
JS::Handle<JSObject*> aKeyframes,
|
|
const UnrestrictedDoubleOrKeyframeEffectOptions& aOptions,
|
|
ErrorResult& aRv)
|
|
{
|
|
return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aTarget,
|
|
aKeyframes, aOptions,
|
|
aRv);
|
|
}
|
|
|
|
/* static */ already_AddRefed<KeyframeEffectReadOnly>
|
|
KeyframeEffectReadOnly::Constructor(const GlobalObject& aGlobal,
|
|
KeyframeEffectReadOnly& aSource,
|
|
ErrorResult& aRv)
|
|
{
|
|
return ConstructKeyframeEffect<KeyframeEffectReadOnly>(aGlobal, aSource, aRv);
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::GetTarget(
|
|
Nullable<OwningElementOrCSSPseudoElement>& aRv) const
|
|
{
|
|
if (!mTarget) {
|
|
aRv.SetNull();
|
|
return;
|
|
}
|
|
|
|
switch (mTarget->mPseudoType) {
|
|
case CSSPseudoElementType::before:
|
|
case CSSPseudoElementType::after:
|
|
aRv.SetValue().SetAsCSSPseudoElement() =
|
|
CSSPseudoElement::GetCSSPseudoElement(mTarget->mElement,
|
|
mTarget->mPseudoType);
|
|
break;
|
|
|
|
case CSSPseudoElementType::NotPseudo:
|
|
aRv.SetValue().SetAsElement() = mTarget->mElement;
|
|
break;
|
|
|
|
default:
|
|
NS_NOTREACHED("Animation of unsupported pseudo-type");
|
|
aRv.SetNull();
|
|
}
|
|
}
|
|
|
|
static void
|
|
CreatePropertyValue(nsCSSPropertyID aProperty,
|
|
float aOffset,
|
|
const Maybe<ComputedTimingFunction>& aTimingFunction,
|
|
const StyleAnimationValue& aValue,
|
|
AnimationPropertyValueDetails& aResult)
|
|
{
|
|
aResult.mOffset = aOffset;
|
|
|
|
nsString stringValue;
|
|
DebugOnly<bool> uncomputeResult =
|
|
StyleAnimationValue::UncomputeValue(aProperty, aValue, stringValue);
|
|
MOZ_ASSERT(uncomputeResult, "failed to uncompute value");
|
|
aResult.mValue = stringValue;
|
|
|
|
if (aTimingFunction) {
|
|
aResult.mEasing.Construct();
|
|
aTimingFunction->AppendToString(aResult.mEasing.Value());
|
|
} else {
|
|
aResult.mEasing.Construct(NS_LITERAL_STRING("linear"));
|
|
}
|
|
|
|
aResult.mComposite = CompositeOperation::Replace;
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::GetProperties(
|
|
nsTArray<AnimationPropertyDetails>& aProperties,
|
|
ErrorResult& aRv) const
|
|
{
|
|
for (const AnimationProperty& property : mProperties) {
|
|
AnimationPropertyDetails propertyDetails;
|
|
propertyDetails.mProperty =
|
|
NS_ConvertASCIItoUTF16(nsCSSProps::GetStringValue(property.mProperty));
|
|
propertyDetails.mRunningOnCompositor = property.mIsRunningOnCompositor;
|
|
|
|
nsXPIDLString localizedString;
|
|
if (property.mPerformanceWarning &&
|
|
property.mPerformanceWarning->ToLocalizedString(localizedString)) {
|
|
propertyDetails.mWarning.Construct(localizedString);
|
|
}
|
|
|
|
if (!propertyDetails.mValues.SetCapacity(property.mSegments.Length(),
|
|
mozilla::fallible)) {
|
|
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return;
|
|
}
|
|
|
|
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.
|
|
propertyDetails.mValues.AppendElement(fromValue, mozilla::fallible);
|
|
|
|
// 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();
|
|
propertyDetails.mValues.AppendElement(toValue, mozilla::fallible);
|
|
}
|
|
}
|
|
|
|
aProperties.AppendElement(propertyDetails);
|
|
}
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::GetKeyframes(JSContext*& aCx,
|
|
nsTArray<JSObject*>& aResult,
|
|
ErrorResult& aRv)
|
|
{
|
|
MOZ_ASSERT(aResult.IsEmpty());
|
|
MOZ_ASSERT(!aRv.Failed());
|
|
|
|
if (!aResult.SetCapacity(mKeyframes.Length(), mozilla::fallible)) {
|
|
aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
|
|
return;
|
|
}
|
|
|
|
for (const Keyframe& keyframe : mKeyframes) {
|
|
// Set up a dictionary object for the explicit members
|
|
BaseComputedKeyframe keyframeDict;
|
|
if (keyframe.mOffset) {
|
|
keyframeDict.mOffset.SetValue(keyframe.mOffset.value());
|
|
}
|
|
MOZ_ASSERT(keyframe.mComputedOffset != Keyframe::kComputedOffsetNotSet,
|
|
"Invalid computed offset");
|
|
keyframeDict.mComputedOffset.Construct(keyframe.mComputedOffset);
|
|
if (keyframe.mTimingFunction) {
|
|
keyframeDict.mEasing.Truncate();
|
|
keyframe.mTimingFunction.ref().AppendToString(keyframeDict.mEasing);
|
|
} // else if null, leave easing as its default "linear".
|
|
|
|
JS::Rooted<JS::Value> keyframeJSValue(aCx);
|
|
if (!ToJSValue(aCx, keyframeDict, &keyframeJSValue)) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
|
|
JS::Rooted<JSObject*> keyframeObject(aCx, &keyframeJSValue.toObject());
|
|
for (const PropertyValuePair& propertyValue : keyframe.mPropertyValues) {
|
|
|
|
const char* name = nsCSSProps::PropertyIDLName(propertyValue.mProperty);
|
|
|
|
// nsCSSValue::AppendToString does not accept shorthands properties but
|
|
// works with token stream values if we pass eCSSProperty_UNKNOWN as
|
|
// the property.
|
|
nsCSSPropertyID propertyForSerializing =
|
|
nsCSSProps::IsShorthand(propertyValue.mProperty)
|
|
? eCSSProperty_UNKNOWN
|
|
: propertyValue.mProperty;
|
|
|
|
nsAutoString stringValue;
|
|
if (propertyValue.mServoDeclarationBlock) {
|
|
Servo_DeclarationBlock_SerializeOneValue(
|
|
propertyValue.mServoDeclarationBlock, &stringValue);
|
|
} else {
|
|
propertyValue.mValue.AppendToString(
|
|
propertyForSerializing, stringValue, nsCSSValue::eNormalized);
|
|
}
|
|
|
|
JS::Rooted<JS::Value> value(aCx);
|
|
if (!ToJSValue(aCx, stringValue, &value) ||
|
|
!JS_DefineProperty(aCx, keyframeObject, name, value,
|
|
JSPROP_ENUMERATE)) {
|
|
aRv.Throw(NS_ERROR_FAILURE);
|
|
return;
|
|
}
|
|
}
|
|
|
|
aResult.AppendElement(keyframeObject);
|
|
}
|
|
}
|
|
|
|
/* 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;
|
|
}
|
|
|
|
bool
|
|
KeyframeEffectReadOnly::CanThrottle() const
|
|
{
|
|
// 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()) {
|
|
return false;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// We can throttle the animation if the animation is paint only and
|
|
// the target frame is out of view or the document is in background tabs.
|
|
if (CanIgnoreIfNotVisible()) {
|
|
nsIPresShell* presShell = GetPresShell();
|
|
if ((presShell && !presShell->IsActive()) ||
|
|
frame->IsScrolledOutOfView()) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// First we need to check layer generation and transform overflow
|
|
// prior to the property.mIsRunningOnCompositor check because we should
|
|
// 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 by !important rules.
|
|
// (GetEffectiveAnimationOfProperty, as called by
|
|
// HasEffectiveAnimationOfProperty, only returns a property which is
|
|
// neither overridden by !important rules nor overridden by other
|
|
// animation.)
|
|
if (!HasEffectiveAnimationOfProperty(record.mProperty)) {
|
|
continue;
|
|
}
|
|
|
|
EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
|
|
mTarget->mPseudoType);
|
|
MOZ_ASSERT(effectSet, "CanThrottle should be called on an effect "
|
|
"associated with a target element");
|
|
layers::Layer* layer =
|
|
FrameLayerBuilder::GetDedicatedLayer(frame, record.mLayerType);
|
|
// Unthrottle if the layer needs to be brought up to date
|
|
if (!layer ||
|
|
effectSet->GetAnimationGeneration() !=
|
|
layer->GetAnimationGeneration()) {
|
|
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;
|
|
}
|
|
}
|
|
|
|
for (const AnimationProperty& property : mProperties) {
|
|
if (!property.mIsRunningOnCompositor) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
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();
|
|
|
|
EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
|
|
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());
|
|
// If this animation can cause overflow, we can throttle some of the ticks.
|
|
if (!animationRuleRefreshTime.IsNull() &&
|
|
(now - animationRuleRefreshTime) < OverflowRegionRefreshInterval()) {
|
|
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;
|
|
}
|
|
|
|
nsIFrame*
|
|
KeyframeEffectReadOnly::GetAnimationFrame() const
|
|
{
|
|
if (!mTarget) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsIFrame* frame = mTarget->mElement->GetPrimaryFrame();
|
|
if (!frame) {
|
|
return nullptr;
|
|
}
|
|
|
|
if (mTarget->mPseudoType == CSSPseudoElementType::before) {
|
|
frame = nsLayoutUtils::GetBeforeFrame(frame);
|
|
} else if (mTarget->mPseudoType == CSSPseudoElementType::after) {
|
|
frame = nsLayoutUtils::GetAfterFrame(frame);
|
|
} else {
|
|
MOZ_ASSERT(mTarget->mPseudoType == CSSPseudoElementType::NotPseudo,
|
|
"unknown mTarget->mPseudoType");
|
|
}
|
|
if (!frame) {
|
|
return nullptr;
|
|
}
|
|
|
|
return nsLayoutUtils::GetStyleFrame(frame);
|
|
}
|
|
|
|
nsIDocument*
|
|
KeyframeEffectReadOnly::GetRenderedDocument() const
|
|
{
|
|
if (!mTarget) {
|
|
return nullptr;
|
|
}
|
|
return mTarget->mElement->GetComposedDoc();
|
|
}
|
|
|
|
nsIPresShell*
|
|
KeyframeEffectReadOnly::GetPresShell() const
|
|
{
|
|
nsIDocument* doc = GetRenderedDocument();
|
|
if (!doc) {
|
|
return nullptr;
|
|
}
|
|
return doc->GetShell();
|
|
}
|
|
|
|
nsPresContext*
|
|
KeyframeEffectReadOnly::GetPresContext() const
|
|
{
|
|
nsIPresShell* shell = GetPresShell();
|
|
if (!shell) {
|
|
return nullptr;
|
|
}
|
|
return shell->GetPresContext();
|
|
}
|
|
|
|
/* static */ bool
|
|
KeyframeEffectReadOnly::IsGeometricProperty(
|
|
const nsCSSPropertyID 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;
|
|
}
|
|
}
|
|
|
|
/* static */ bool
|
|
KeyframeEffectReadOnly::CanAnimateTransformOnCompositor(
|
|
const nsIFrame* aFrame,
|
|
AnimationPerformanceWarning::Type& aPerformanceWarning)
|
|
{
|
|
// Disallow OMTA for preserve-3d transform. Note that we check the style property
|
|
// rather than Extend3DContext() since that can recurse back into this function
|
|
// via HasOpacity(). See bug 779598.
|
|
if (aFrame->Combines3DTransformWithAncestors() ||
|
|
aFrame->StyleDisplay()->mTransformStyle == NS_STYLE_TRANSFORM_STYLE_PRESERVE_3D) {
|
|
aPerformanceWarning = AnimationPerformanceWarning::Type::TransformPreserve3D;
|
|
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
|
|
// looking at backface-visibility on descendants as well. See bug 1186204.
|
|
if (aFrame->BackfaceIsHidden()) {
|
|
aPerformanceWarning =
|
|
AnimationPerformanceWarning::Type::TransformBackfaceVisibilityHidden;
|
|
return false;
|
|
}
|
|
// Async 'transform' animations of aFrames with SVG transforms is not
|
|
// supported. See bug 779599.
|
|
if (aFrame->IsSVGTransformed()) {
|
|
aPerformanceWarning = AnimationPerformanceWarning::Type::TransformSVG;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
KeyframeEffectReadOnly::ShouldBlockAsyncTransformAnimations(
|
|
const nsIFrame* aFrame,
|
|
AnimationPerformanceWarning::Type& aPerformanceWarning) const
|
|
{
|
|
// We currently only expect this method to be called for effects whose
|
|
// animations are eligible for the compositor since, Animations that are
|
|
// paused, zero-duration, finished etc. should not block other animations from
|
|
// running on the compositor.
|
|
MOZ_ASSERT(mAnimation && mAnimation->IsPlayableOnCompositor());
|
|
|
|
EffectSet* effectSet =
|
|
EffectSet::GetEffectSet(mTarget->mElement, mTarget->mPseudoType);
|
|
for (const AnimationProperty& property : mProperties) {
|
|
// If there is a property for animations level that is overridden by
|
|
// !important rules, it should not block other animations from running
|
|
// on the compositor.
|
|
// NOTE: We don't currently check for !important rules for properties that
|
|
// don't run on the compositor. As result such properties (e.g. margin-left)
|
|
// can still block async animations even if they are overridden by
|
|
// !important rules.
|
|
if (effectSet &&
|
|
effectSet->PropertiesWithImportantRules()
|
|
.HasProperty(property.mProperty) &&
|
|
effectSet->PropertiesForAnimationsLevel()
|
|
.HasProperty(property.mProperty)) {
|
|
continue;
|
|
}
|
|
// Check for geometric properties
|
|
if (IsGeometricProperty(property.mProperty)) {
|
|
aPerformanceWarning =
|
|
AnimationPerformanceWarning::Type::TransformWithGeometricProperties;
|
|
return true;
|
|
}
|
|
|
|
// Check for unsupported transform animations
|
|
if (property.mProperty == eCSSProperty_transform) {
|
|
if (!CanAnimateTransformOnCompositor(aFrame,
|
|
aPerformanceWarning)) {
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::SetPerformanceWarning(
|
|
nsCSSPropertyID aProperty,
|
|
const AnimationPerformanceWarning& aWarning)
|
|
{
|
|
for (AnimationProperty& property : mProperties) {
|
|
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);
|
|
AnimationUtils::LogAsyncAnimationFailure(logMessage, mTarget->mElement);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
static already_AddRefed<nsStyleContext>
|
|
CreateStyleContextForAnimationValue(nsCSSPropertyID aProperty,
|
|
const StyleAnimationValue& aValue,
|
|
nsStyleContext* aBaseStyleContext)
|
|
{
|
|
MOZ_ASSERT(aBaseStyleContext,
|
|
"CreateStyleContextForAnimationValue needs to be called "
|
|
"with a valid nsStyleContext");
|
|
|
|
RefPtr<AnimValuesStyleRule> styleRule = new AnimValuesStyleRule();
|
|
styleRule->AddValue(aProperty, aValue);
|
|
|
|
nsCOMArray<nsIStyleRule> rules;
|
|
rules.AppendObject(styleRule);
|
|
|
|
MOZ_ASSERT(aBaseStyleContext->PresContext()->StyleSet()->IsGecko(),
|
|
"ServoStyleSet should not use StyleAnimationValue for animations");
|
|
nsStyleSet* styleSet =
|
|
aBaseStyleContext->PresContext()->StyleSet()->AsGecko();
|
|
|
|
RefPtr<nsStyleContext> styleContext =
|
|
styleSet->ResolveStyleByAddingRules(aBaseStyleContext, rules);
|
|
|
|
// We need to call StyleData to generate cached data for the style context.
|
|
// Otherwise CalcStyleDifference returns no meaningful result.
|
|
styleContext->StyleData(nsCSSProps::kSIDTable[aProperty]);
|
|
|
|
return styleContext.forget();
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::CalculateCumulativeChangeHint(
|
|
nsStyleContext *aStyleContext)
|
|
{
|
|
mCumulativeChangeHint = nsChangeHint(0);
|
|
|
|
for (const AnimationProperty& property : mProperties) {
|
|
for (const AnimationPropertySegment& segment : property.mSegments) {
|
|
RefPtr<nsStyleContext> fromContext =
|
|
CreateStyleContextForAnimationValue(property.mProperty,
|
|
segment.mFromValue, aStyleContext);
|
|
|
|
RefPtr<nsStyleContext> toContext =
|
|
CreateStyleContextForAnimationValue(property.mProperty,
|
|
segment.mToValue, aStyleContext);
|
|
|
|
uint32_t equalStructs = 0;
|
|
uint32_t samePointerStructs = 0;
|
|
nsChangeHint changeHint =
|
|
fromContext->CalcStyleDifference(toContext,
|
|
nsChangeHint(0),
|
|
&equalStructs,
|
|
&samePointerStructs);
|
|
|
|
mCumulativeChangeHint |= changeHint;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::SetAnimation(Animation* aAnimation)
|
|
{
|
|
if (mAnimation == aAnimation) {
|
|
return;
|
|
}
|
|
|
|
// Restyle for the old animation.
|
|
RequestRestyle(EffectCompositor::RestyleType::Layer);
|
|
|
|
mAnimation = aAnimation;
|
|
|
|
// The order of these function calls is important:
|
|
// NotifyAnimationTimingUpdated() need the updated mIsRelevant flag to check
|
|
// if it should create the effectSet or not, and MarkCascadeNeedsUpdate()
|
|
// needs a valid effectSet, so we should call them in this order.
|
|
if (mAnimation) {
|
|
mAnimation->UpdateRelevance();
|
|
}
|
|
NotifyAnimationTimingUpdated();
|
|
if (mAnimation) {
|
|
MarkCascadeNeedsUpdate();
|
|
}
|
|
}
|
|
|
|
bool
|
|
KeyframeEffectReadOnly::CanIgnoreIfNotVisible() const
|
|
{
|
|
if (!AnimationUtils::IsOffscreenThrottlingEnabled()) {
|
|
return false;
|
|
}
|
|
|
|
// FIXME (bug 1303235): We don't calculate mCumulativeChangeHint for
|
|
// the Servo backend yet
|
|
nsPresContext* presContext = GetPresContext();
|
|
if (!presContext || presContext->StyleSet()->IsServo()) {
|
|
return false;
|
|
}
|
|
|
|
// FIXME: For further sophisticated optimization we need to check
|
|
// change hint on the segment corresponding to computedTiming.progress.
|
|
return NS_IsHintSubset(
|
|
mCumulativeChangeHint, nsChangeHint_Hints_CanIgnoreIfNotVisible);
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::MaybeUpdateFrameForCompositor()
|
|
{
|
|
nsIFrame* frame = GetAnimationFrame();
|
|
if (!frame) {
|
|
return;
|
|
}
|
|
|
|
// FIXME: Bug 1272495: If this effect does not win in the cascade, the
|
|
// NS_FRAME_MAY_BE_TRANSFORMED flag should be removed when the animation
|
|
// will be removed from effect set or the transform keyframes are removed
|
|
// by setKeyframes. The latter case will be hard to solve though.
|
|
for (const AnimationProperty& property : mProperties) {
|
|
if (property.mProperty == eCSSProperty_transform) {
|
|
frame->AddStateBits(NS_FRAME_MAY_BE_TRANSFORMED);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void
|
|
KeyframeEffectReadOnly::MarkCascadeNeedsUpdate()
|
|
{
|
|
if (!mTarget) {
|
|
return;
|
|
}
|
|
|
|
EffectSet* effectSet = EffectSet::GetEffectSet(mTarget->mElement,
|
|
mTarget->mPseudoType);
|
|
if (!effectSet) {
|
|
return;
|
|
}
|
|
effectSet->MarkCascadeNeedsUpdate();
|
|
}
|
|
|
|
bool
|
|
KeyframeEffectReadOnly::HasComputedTimingChanged() const
|
|
{
|
|
// Typically we don't need to request a restyle if the progress hasn't
|
|
// changed since the last call to ComposeStyle. The one exception is if the
|
|
// iteration composite mode is 'accumulate' and the current iteration has
|
|
// changed, since that will often produce a different result.
|
|
ComputedTiming computedTiming = GetComputedTiming();
|
|
return computedTiming.mProgress != mProgressOnLastCompose ||
|
|
(mEffectOptions.mIterationComposite ==
|
|
IterationCompositeOperation::Accumulate &&
|
|
computedTiming.mCurrentIteration !=
|
|
mCurrentIterationOnLastCompose);
|
|
}
|
|
|
|
} // namespace dom
|
|
} // namespace mozilla
|