Files

660 lines
25 KiB
C++
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "CSSAnimation.h"
#include "mozilla/AnimationEventDispatcher.h"
#include "mozilla/KeyframeUtils.h"
#include "mozilla/TimeStamp.h"
#include "mozilla/dom/CSSAnimationBinding.h"
#include "mozilla/dom/KeyframeEffectBinding.h"
#include "nsPresContext.h"
namespace mozilla::dom {
using AnimationPhase = ComputedTiming::AnimationPhase;
JSObject* CSSAnimation::WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) {
return dom::CSSAnimation_Binding::Wrap(aCx, this, aGivenProto);
}
void CSSAnimation::SetEffect(AnimationEffect* aEffect) {
Animation::SetEffect(aEffect);
PropertiesWillSetFromJS(CSSAnimationProperties::Effect);
}
void CSSAnimation::SetStartTime(const Nullable<CSSNumberish>& aStartTime,
ErrorResult& aRv) {
// Note that we always compare with the paused state since for the purposes
// of determining if play control is being overridden or not, we want to
// treat the finished state as running.
bool wasPaused = PlayState() == AnimationPlayState::Paused;
Animation::SetStartTime(aStartTime, aRv);
if (aRv.Failed()) {
return;
}
bool isPaused = PlayState() == AnimationPlayState::Paused;
if (wasPaused != isPaused) {
PropertiesWillSetFromJS(CSSAnimationProperties::PlayState);
}
}
mozilla::dom::Promise* CSSAnimation::GetReady(ErrorResult& aRv) {
FlushUnanimatedStyle();
return Animation::GetReady(aRv);
}
void CSSAnimation::Reverse(ErrorResult& aRv) {
// As with CSSAnimation::SetStartTime, we're really only interested in the
// paused state.
bool wasPaused = PlayState() == AnimationPlayState::Paused;
Animation::Reverse(aRv);
if (aRv.Failed()) {
return;
}
bool isPaused = PlayState() == AnimationPlayState::Paused;
if (wasPaused != isPaused) {
PropertiesWillSetFromJS(CSSAnimationProperties::PlayState);
}
}
AnimationPlayState CSSAnimation::PlayStateFromJS() const {
// Flush style to ensure that any properties controlling animation state
// (e.g. animation-play-state) are fully updated.
FlushUnanimatedStyle();
return Animation::PlayStateFromJS();
}
bool CSSAnimation::PendingFromJS() const {
// Flush style since, for example, if the animation-play-state was just
// changed its possible we should now be pending.
FlushUnanimatedStyle();
return Animation::PendingFromJS();
}
void CSSAnimation::PlayFromJS(ErrorResult& aRv) {
// Note that flushing style below might trigger calls to
// PlayFromStyle()/PauseFromStyle() on this object.
FlushUnanimatedStyle();
Animation::PlayFromJS(aRv);
if (aRv.Failed()) {
return;
}
PropertiesWillSetFromJS(CSSAnimationProperties::PlayState);
}
void CSSAnimation::PauseFromJS(ErrorResult& aRv) {
Animation::PauseFromJS(aRv);
if (aRv.Failed()) {
return;
}
PropertiesWillSetFromJS(CSSAnimationProperties::PlayState);
}
void CSSAnimation::PlayFromStyle() {
ErrorResult rv;
Animation::Play(rv, Animation::LimitBehavior::Continue);
// play() should not throw when LimitBehavior is Continue
MOZ_ASSERT(!rv.Failed(), "Unexpected exception playing animation");
}
void CSSAnimation::PauseFromStyle() {
ErrorResult rv;
Animation::Pause(rv);
// pause() should only throw when *all* of the following conditions are true:
// - we are in the idle state, and
// - we have a negative playback rate, and
// - we have an infinitely repeating animation
// The first two conditions will never happen under regular style processing
// but could happen if an author made modifications to the Animation object
// and then updated animation-play-state. It's an unusual case and there's
// no obvious way to pass on the exception information so we just silently
// fail for now.
if (rv.Failed()) {
NS_WARNING("Unexpected exception pausing animation - silently failing");
}
}
void CSSAnimation::Tick(TickState& aState) {
Animation::Tick(aState);
QueueEvents();
}
int32_t CSSAnimation::CompareCompositeOrder(
const CSSAnimation& aOther, nsContentUtils::NodeIndexCache& aCache) const {
MOZ_ASSERT(IsTiedToMarkup() && aOther.IsTiedToMarkup(),
"Should only be called for CSS animations that are sorted "
"as CSS animations (i.e. tied to CSS markup)");
// 0. Object-equality case
if (&aOther == this) {
return 0;
}
// 1. Sort by document order
if (!mOwningElement.Equals(aOther.mOwningElement)) {
return mOwningElement.Compare(aOther.mOwningElement, aCache);
}
// 2. (Same element and pseudo): Sort by position in animation-name
MOZ_ASSERT(mAnimationIndex != aOther.mAnimationIndex);
return mAnimationIndex < aOther.mAnimationIndex ? -1 : 1;
}
void CSSAnimation::QueueEvents(const StickyTimeDuration& aActiveTime) {
// If the animation is pending, we ignore animation events until we finish
// pending.
if (mPendingState != PendingState::NotPending) {
return;
}
// CSS animations dispatch events at their owning element. This allows
// script to repurpose a CSS animation to target a different element,
// to use a group effect (which has no obvious "target element"), or
// to remove the animation effect altogether whilst still getting
// animation events.
//
// It does mean, however, that for a CSS animation that has no owning
// element (e.g. it was created using the CSSAnimation constructor or
// disassociated from CSS) no events are fired. If it becomes desirable
// for these animations to still fire events we should spec the concept
// of the "original owning element" or "event target" and allow script
// to set it when creating a CSSAnimation object.
if (!mOwningElement.ShouldFireEvents()) {
return;
}
nsPresContext* presContext = mOwningElement.GetPresContext();
if (!presContext) {
return;
}
uint64_t currentIteration = 0;
ComputedTiming::AnimationPhase currentPhase;
StickyTimeDuration intervalStartTime;
StickyTimeDuration intervalEndTime;
StickyTimeDuration iterationStartTime;
if (!mEffect) {
currentPhase =
GetAnimationPhaseWithoutEffect<ComputedTiming::AnimationPhase>(*this);
if (currentPhase == mPreviousPhase) {
return;
}
} else {
ComputedTiming computedTiming = mEffect->GetComputedTiming();
currentPhase = computedTiming.mPhase;
currentIteration = computedTiming.mCurrentIteration;
if (currentPhase == mPreviousPhase &&
currentIteration == mPreviousIteration) {
return;
}
intervalStartTime = IntervalStartTime(computedTiming.mActiveDuration);
intervalEndTime = IntervalEndTime(computedTiming.mActiveDuration);
uint64_t iterationBoundary = mPreviousIteration > currentIteration
? currentIteration + 1
: currentIteration;
double multiplier = iterationBoundary - computedTiming.mIterationStart;
if (multiplier != 0.0) {
iterationStartTime = computedTiming.mDuration.MultDouble(multiplier);
}
}
TimeStamp startTimeStamp = ElapsedTimeToTimeStamp(intervalStartTime);
TimeStamp endTimeStamp = ElapsedTimeToTimeStamp(intervalEndTime);
TimeStamp iterationTimeStamp = ElapsedTimeToTimeStamp(iterationStartTime);
AutoTArray<AnimationEventInfo, 2> events;
auto appendAnimationEvent = [&](EventMessage aMessage,
const StickyTimeDuration& aElapsedTime,
const TimeStamp& aScheduledEventTimeStamp) {
double elapsedTime = aElapsedTime.ToSeconds();
if (aMessage == eAnimationCancel) {
// 0 is an inappropriate value for this callsite. What we need to do is
// use a single random value for all increasing times reportable.
// That is to say, whenever elapsedTime goes negative (because an
// animation restarts, something rewinds the animation, or otherwise)
// a new random value for the mix-in must be generated.
elapsedTime = nsRFPService::ReduceTimePrecisionAsSecsRFPOnly(
elapsedTime, 0, mRTPCallerType);
}
events.AppendElement(AnimationEventInfo(
mAnimationName, mOwningElement.Target(), aMessage, elapsedTime,
mAnimationIndex, aScheduledEventTimeStamp, this));
};
// Handle cancel event first
if ((mPreviousPhase != AnimationPhase::Idle &&
mPreviousPhase != AnimationPhase::After) &&
currentPhase == AnimationPhase::Idle) {
appendAnimationEvent(eAnimationCancel, aActiveTime,
GetTimelineCurrentTimeAsTimeStamp());
}
switch (mPreviousPhase) {
case AnimationPhase::Idle:
case AnimationPhase::Before:
if (currentPhase == AnimationPhase::Active) {
appendAnimationEvent(eAnimationStart, intervalStartTime,
startTimeStamp);
} else if (currentPhase == AnimationPhase::After) {
appendAnimationEvent(eAnimationStart, intervalStartTime,
startTimeStamp);
appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
}
break;
case AnimationPhase::Active:
if (currentPhase == AnimationPhase::Before) {
appendAnimationEvent(eAnimationEnd, intervalStartTime, startTimeStamp);
} else if (currentPhase == AnimationPhase::Active) {
// The currentIteration must have changed or element we would have
// returned early above.
MOZ_ASSERT(currentIteration != mPreviousIteration);
appendAnimationEvent(eAnimationIteration, iterationStartTime,
iterationTimeStamp);
} else if (currentPhase == AnimationPhase::After) {
appendAnimationEvent(eAnimationEnd, intervalEndTime, endTimeStamp);
}
break;
case AnimationPhase::After:
if (currentPhase == AnimationPhase::Before) {
appendAnimationEvent(eAnimationStart, intervalEndTime, startTimeStamp);
appendAnimationEvent(eAnimationEnd, intervalStartTime, endTimeStamp);
} else if (currentPhase == AnimationPhase::Active) {
appendAnimationEvent(eAnimationStart, intervalEndTime, endTimeStamp);
}
break;
}
mPreviousPhase = currentPhase;
mPreviousIteration = currentIteration;
if (!events.IsEmpty()) {
presContext->AnimationEventDispatcher()->QueueEvents(std::move(events));
}
}
void CSSAnimation::UpdateTiming(SeekFlag aSeekFlag,
SyncNotifyFlag aSyncNotifyFlag) {
if (mNeedsNewAnimationIndexWhenRun &&
PlayState() != AnimationPlayState::Idle) {
mAnimationIndex = sNextAnimationIndex++;
mNeedsNewAnimationIndexWhenRun = false;
}
Animation::UpdateTiming(aSeekFlag, aSyncNotifyFlag);
}
/////////////////////// CSSAnimationKeyframeEffect ////////////////////////
void CSSAnimationKeyframeEffect::GetTiming(EffectTiming& aRetVal) const {
MaybeFlushUnanimatedStyle();
KeyframeEffect::GetTiming(aRetVal);
}
void CSSAnimationKeyframeEffect::GetComputedTimingAsDict(
ComputedEffectTiming& aRetVal) const {
MaybeFlushUnanimatedStyle();
KeyframeEffect::GetComputedTimingAsDict(aRetVal);
}
void CSSAnimationKeyframeEffect::UpdateTiming(
const OptionalEffectTiming& aTiming, ErrorResult& aRv) {
KeyframeEffect::UpdateTiming(aTiming, aRv);
if (aRv.Failed()) {
return;
}
if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
CSSAnimationProperties updatedProperties = CSSAnimationProperties::None;
if (aTiming.mDuration.WasPassed()) {
updatedProperties |= CSSAnimationProperties::Duration;
}
if (aTiming.mIterations.WasPassed()) {
updatedProperties |= CSSAnimationProperties::IterationCount;
}
if (aTiming.mDirection.WasPassed()) {
updatedProperties |= CSSAnimationProperties::Direction;
}
if (aTiming.mDelay.WasPassed()) {
updatedProperties |= CSSAnimationProperties::Delay;
}
if (aTiming.mFill.WasPassed()) {
updatedProperties |= CSSAnimationProperties::FillMode;
}
cssAnimation->PropertiesWillSetFromJS(updatedProperties);
}
}
void CSSAnimationKeyframeEffect::SetKeyframes(JSContext* aContext,
JS::Handle<JSObject*> aKeyframes,
ErrorResult& aRv) {
nsTArray<Keyframe> keyframes = KeyframeUtils::GetKeyframesFromObject(
aContext, mDocument, aKeyframes, "KeyframeEffect.setKeyframes", aRv);
if (aRv.Failed()) {
return;
}
// SetKeyframes below will trigger the keyframe generation (for missing 0% and
// 100%), so we set this flag to avoid generating the keyframes if they come
// from JS.
// Note that this is different from CSSAnimationProperties::Keyframes below
// because we have to avoid generating keyframes even if we don't have
// animation, i.e. this is a keyframe-effect-specific flag for keyframes
// generation.
mIgnoreKeyframesGeneration = true;
// This is from the JS API, so we use the associated animation's timeline and
// range if any.
RefPtr<const ComputedStyle> style = GetTargetComputedStyle(Flush::None);
KeyframeEffect::SetKeyframes(
std::move(keyframes), style,
mAnimation ? mAnimation->GetTimeline() : nullptr,
mAnimation ? &mAnimation->GetTimelineRange() : nullptr);
if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
cssAnimation->PropertiesWillSetFromJS(CSSAnimationProperties::Keyframes);
}
}
void CSSAnimationKeyframeEffect::SetComposite(
const CompositeOperation& aComposite) {
KeyframeEffect::SetComposite(aComposite);
if (CSSAnimation* cssAnimation = GetOwningCSSAnimation()) {
cssAnimation->PropertiesWillSetFromJS(CSSAnimationProperties::Composition);
}
}
class ComputedOffsetComparator {
public:
static bool LessThan(const Keyframe& aLhs, const Keyframe& aRhs) {
// Keep the specified order if any of the keyframes have no computed offset.
return !std::isnan(aLhs.mComputedOffset) &&
!std::isnan(aRhs.mComputedOffset) &&
aLhs.mComputedOffset < aRhs.mComputedOffset;
}
};
enum class TargetKeyframe : uint8_t {
Initial,
Final,
};
static Keyframe* GetOrCreateInitialOrFinalComputedKeyframe(
nsTArray<Keyframe>& aPercentageKeyframes,
nsTArray<Keyframe>& aRangeKeyframes, const TargetKeyframe aTargetKeyframe,
const StyleComputedTimingFunction& aDefaultTimingFunction,
const CompositeOperationOrAuto aDefaultComposite) {
const double targetOffset =
aTargetKeyframe == TargetKeyframe::Initial ? 0.0 : 1.0;
auto IsKeyframeMatched = [&](const Keyframe& aKeyframe) {
return aKeyframe.mComputedOffset == targetOffset &&
(aKeyframe.mComposite == CompositeOperationOrAuto::Auto ||
aKeyframe.mComposite == aDefaultComposite) &&
(aKeyframe.mTimingFunction
? *aKeyframe.mTimingFunction == aDefaultTimingFunction
: aDefaultTimingFunction.IsLinearKeyword());
};
// 1. Search the offset in |aRangeKeyframes| first, in the reverse order. We
// search |aRangeKeyframes| first because we perfer the later keyframe in
// the used order, which breaks the tie with the computed order. And range
// offsets are always put after percentage offsets in the computed order.
//
// Note: We intentionally search in the reverse order because we prefer the
// later keyframe if mulitple keyframes have 0% or 100% (with the matched
// easing and composite). It should happen only in keyframes with
// <timeline-range-offset> offsets because we don't group them when converting
// them from Servo |KeyframesStep| into Gecko |Keyframe|, in
// Servo_StyleSet_GetKeyframesForName(), and intentionally keep those
// keyframes unsorted (i.e. the specified order).
// e.g.
// cover 0% {...}
// entry 0% {...}
// Both of them have computed offset 0%. Since we keep |mKeyframes| with
// <timeline-range-offset> offsets unsorted, so we can simply choose the last
// one when breaking a tie.
//
// This is spec'ed especially for 100%. For 0%, the spec doesnt' mention that
// so we use the same rule for now.
// https://github.com/w3c/csswg-drafts/issues/14281
for (auto& keyframe : Reversed(aRangeKeyframes)) {
MOZ_ASSERT(keyframe.mOffset);
// Skip the keyframe with unresolved computed offset.
if (std::isnan(keyframe.mComputedOffset)) {
continue;
}
if (IsKeyframeMatched(keyframe)) {
return &keyframe;
}
// |aRangeKeyframes| is sorted by |keyframe.mComputedOffset|, so we can
// break early.
if (keyframe.mComputedOffset < targetOffset) {
break;
}
}
// 2. Search the offset in |aPercentageKeyframes|. We group the keyframes with
// <percentage> offsets already so we shouldn't have mulitple matches.
//
// Note: |insertPosition| is used to find the last 0% or 100% keyframe. We
// would like to insert the missing keyframe after it.
// e.g.
// 0% { animation-timing-function: linear; }
// 50% { width: 100px; }
// 100% { animation-composition: add; }
// We have to generate the keyframes like this:
// [0] { offset: 0.0, easing: 'linear', composite: 'auto' }
// [1] { offset: 0.0, easing: 'ease', composite: 'replace' } // generated 0%
// [2] { offset: 0.5, easing: 'ease', composite: 'auto' }
// [3] { offset: 1.0, easing: 'ease', composite: 'add' }
// [4] { offset: 1.0, easing: 'ease', composite: 'replace' } // generated 100%
// So |insertPosition| for 0% is 1, and for 100% is 4.
size_t insertPosition = 0;
for (auto& keyframe : aPercentageKeyframes) {
MOZ_ASSERT(keyframe.mOffset);
MOZ_ASSERT(!std::isnan(keyframe.mComputedOffset));
if (IsKeyframeMatched(keyframe)) {
return &keyframe;
}
if (keyframe.mComputedOffset > targetOffset) {
break;
}
++insertPosition;
}
// Now this is a missing 0%/100% keyframe. Let's generate it.
Keyframe* newKeyframe = aPercentageKeyframes.InsertElementAt(insertPosition);
newKeyframe->mOffset.emplace(
Keyframe::OffsetType::PercentageOffset(targetOffset));
newKeyframe->mComputedOffset = targetOffset;
if (!aDefaultTimingFunction.IsLinearKeyword()) {
newKeyframe->mTimingFunction.emplace(aDefaultTimingFunction);
}
newKeyframe->mComposite = aDefaultComposite;
return newKeyframe;
}
// The step 3 (Generate Initial and Final Frames) in
// https://drafts.csswg.org/css-animations-2/#keyframe-processing
//
// Note: we use "computed keyframes" here because we expect those keyframe
// offsets have been resolved (so we have resolved computed offsets here for the
// active timeline). The only difference from spec is that we don't compute the
// property values here since the caller will do that. Perhaps we could merge
// them together in the future.
// https://drafts.csswg.org/web-animations-1/#computed-keyframes
bool CSSAnimationKeyframeEffect::GetComputedKeyframes(
nsTArray<Keyframe>& aKeyframes) const {
// Check if we do need to do this. This flag is set if the keyframes come from
// the JS API.
if (mIgnoreKeyframesGeneration) {
return false;
}
// FIXME: Perhaps we could cache |allProperties| for keyframes with
// <percentage> offsets. (The properties of keyframes with
// <timeline-range-offset> offsets need to be checked everytime
// unfortunately.)
AnimatedPropertyIDSet allProperties;
AnimatedPropertyIDSet fromProperties;
AnimatedPropertyIDSet toProperties;
// Add 2 more elements for the insertion of 0% and 100% if any.
nsTArray<Keyframe> percentageKeyframes(mKeyframes.Length() + 2);
nsTArray<Keyframe> rangeKeyframes(mKeyframes.Length());
// Iterate keyframes and collect properties first. We have to append the
// propeties to the 0% or 100% keyframes if needed. We merged keyframes and
// converted the properties into physical longhands for the specified
// keyframes already, so we can just collect the propertis information here.
for (const Keyframe& keyframe : mKeyframes) {
MOZ_ASSERT(keyframe.mOffset);
if (keyframe.mOffset->IsPercentageOffset()) {
percentageKeyframes.AppendElement(keyframe);
} else {
rangeKeyframes.AppendElement(keyframe);
}
if (std::isnan(keyframe.mComputedOffset)) {
continue;
}
AnimatedPropertyIDSet currentProperties;
for (const auto& pair : keyframe.mPropertyValues) {
MOZ_ASSERT(
!pair.mProperty.IsShorthand(),
"The shorthands should be expanded already for CSS Animations");
MOZ_ASSERT(KeyframeUtils::IsAnimatableProperty(pair.mProperty),
"It should be animatable for CSS Animations");
currentProperties.AddProperty(pair.mProperty);
}
allProperties.AddProperties(currentProperties);
// Note that the automatic from (0%) and to (100%) keyframes are only
// generated for properties that dont have keyframes at or earlier than 0%
// or at or after 100% (respectively).
// https://drafts.csswg.org/scroll-animations-1/#named-range-keyframes
//
// So we have to collect the properties with offsets <= 0 and offsets >= 1,
// no matter what keyframe-specific timing function and keyframe-specific
// composite are used.
if (keyframe.mComputedOffset <= 0.0) {
fromProperties.AddProperties(currentProperties);
} else if (keyframe.mComputedOffset >= 1.0) {
toProperties.AddProperties(currentProperties);
}
}
// We keep the specified order if all of them are unresolved. Otherwise, sort
// them with computed offsets to get the used order. We do this for finding
// and generating the initial and final keyframes (since we need the used
// order to search the keyframes below).
//
// Also, in getKeyframes(), other browsers show the keyframes with
// <timeline-range-offset> offsets in the order of the computed offsets. This
// is vague in the spec after we introduced <timeline-range-offset>, so we
// just match the behavior of other browsers here.
rangeKeyframes.StableSort(ComputedOffsetComparator());
// The default composite used to search to 0% and 100% keyframes below.
const dom::CompositeOperationOrAuto defaultComposite = [&]() {
switch (mDefaultComposite) {
case CompositeOperation::Replace:
return dom::CompositeOperationOrAuto::Replace;
case CompositeOperation::Add:
return dom::CompositeOperationOrAuto::Add;
case CompositeOperation::Accumulate:
return dom::CompositeOperationOrAuto::Accumulate;
}
// The initial value is replace.
return dom::CompositeOperationOrAuto::Replace;
}();
// Generate Initial and Final Frames:
// 1. Find or create the initial keyframe, a keyframe with a keyframe offset
// of 0%, default timing function as its keyframe timing function, and
// default composite as its keyframe composite.
// 2. For any property in animated properties that is not otherwise present
// in a keyframe with an offset of 0% or one that would be positioned
// earlier in the used keyframe order, add the computed value of that
// property on element to initial keyframes keyframe values.
// 3. If initial keyframes keyframe values is not empty, prepend initial
// keyframe to keyframes. (Note: We add it to keyframes after the last
// keyframe with offset 0%.)
const bool isEmptyKeyframes =
percentageKeyframes.IsEmpty() && rangeKeyframes.IsEmpty();
auto appendProperties = [&](Keyframe& aKeyframe,
const AnimatedPropertyIDSet& aCurrentProperties) {
auto& propertyValues = aKeyframe.mPropertyValues;
for (const auto& property : allProperties) {
if (aCurrentProperties.HasProperty(property)) {
continue;
}
// The nullptr of |mServoDeclarationBlock| means we use the base values,
// i.e. the value generated for that property by finding the computed
// value for that property in the absence of animations.
propertyValues.AppendElement(PropertyValuePair{property, nullptr});
}
};
// Check the equality of two property sets. Note that |fromProperties| must be
// a subset of |allProperties|.
if (!allProperties.IsSubsetOf(fromProperties) && !isEmptyKeyframes) {
Keyframe* fromKeyframe = GetOrCreateInitialOrFinalComputedKeyframe(
percentageKeyframes, rangeKeyframes, TargetKeyframe::Initial,
mDefaultTimingFunction, defaultComposite);
MOZ_ASSERT(fromKeyframe);
appendProperties(*fromKeyframe, fromProperties);
}
// 4. Repeat for final keyframe, using an offset of 100%, considering
// keyframes positioned later in the used keyframe order, and appending to
// keyframes. Note that |toProperties| must be a subset of |allProperties|.
if (!allProperties.IsSubsetOf(toProperties) && !isEmptyKeyframes) {
Keyframe* toKeyframe = GetOrCreateInitialOrFinalComputedKeyframe(
percentageKeyframes, rangeKeyframes, TargetKeyframe::Final,
mDefaultTimingFunction, defaultComposite);
MOZ_ASSERT(toKeyframe);
appendProperties(*toKeyframe, toProperties);
}
aKeyframes = std::move(percentageKeyframes);
if (!rangeKeyframes.IsEmpty()) {
aKeyframes.AppendElements(std::move(rangeKeyframes));
}
return true;
}
void CSSAnimationKeyframeEffect::MaybeFlushUnanimatedStyle() const {
if (!GetOwningCSSAnimation()) {
return;
}
if (dom::Document* doc = GetRenderedDocument()) {
doc->FlushPendingNotifications(
ChangesToFlush(FlushType::Style, false /* flush animations */, false));
}
}
} // namespace mozilla::dom