Files
Boris Chiou 975b0611f7 Bug 2037642 - Implement the generation of initial and final keyframes for animated properties. r=layout-scroll-driven-animation-reviewers,dshin
This implements the spec: the step 3 (i.e. Generate Initial and Final Frames)
in [1], and it handles the offsets with `<= 0` and `>= 1` based on [2].

I comment out the caller because we are still generating the initial
and final keyframes and filling the missing properties in
`Servo_StyleSet_GetKeyframesForName()` (to the specified keyframes).

[1] https://drafts.csswg.org/css-animations-2/#keyframe-processing
[2] https://drafts.csswg.org/scroll-animations-1/#named-range-keyframes

No behavior change.

Differential Revision: https://phabricator.services.mozilla.com/D316399
2026-08-06 01:12:36 +00:00

258 lines
10 KiB
C++

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#ifndef mozilla_dom_CSSAnimation_h
#define mozilla_dom_CSSAnimation_h
#include "AnimationCommon.h"
#include "mozilla/StyleAnimationValue.h"
#include "mozilla/dom/Animation.h"
#include "mozilla/dom/KeyframeEffect.h"
#include "mozilla/dom/MutationObservers.h"
namespace mozilla {
namespace dom {
class CSSAnimation final : public Animation {
public:
explicit CSSAnimation(nsIGlobalObject* aGlobal, nsAtom* aAnimationName)
: dom::Animation(aGlobal),
mAnimationName(aAnimationName),
mNeedsNewAnimationIndexWhenRun(false),
mPreviousPhase(ComputedTiming::AnimationPhase::Idle),
mPreviousIteration(0) {
// We might need to drop this assertion once we add a script-accessible
// constructor but for animations generated from CSS markup the
// animation-name should never be empty.
MOZ_ASSERT(mAnimationName != nsGkAtoms::_empty,
"animation-name should not be 'none'");
}
JSObject* WrapObject(JSContext* aCx,
JS::Handle<JSObject*> aGivenProto) override;
CSSAnimation* AsCSSAnimation() override { return this; }
const CSSAnimation* AsCSSAnimation() const override { return this; }
// CSSAnimation interface
void GetAnimationName(nsString& aRetVal) const {
mAnimationName->ToString(aRetVal);
}
nsAtom* AnimationName() const { return mAnimationName; }
// Animation interface overrides
void SetEffect(AnimationEffect* aEffect) override;
void SetStartTime(const Nullable<CSSNumberish>& aStartTime,
ErrorResult& aRv) override;
Promise* GetReady(ErrorResult& aRv) override;
void Reverse(ErrorResult& aRv) override;
// NOTE: tabbrowser.xml currently relies on the fact that reading the
// currentTime of a CSSAnimation does *not* flush style (whereas reading the
// playState does). If CSS Animations 2 specifies that reading currentTime
// also flushes style we will need to find another way to detect canceled
// animations in tabbrowser.xml. On the other hand, if CSS Animations 2
// specifies that reading playState does *not* flush style (and we drop the
// following override), then we should update tabbrowser.xml to check
// the playState instead.
AnimationPlayState PlayStateFromJS() const override;
bool PendingFromJS() const override;
void PlayFromJS(ErrorResult& aRv) override;
void PauseFromJS(ErrorResult& aRv) override;
void PlayFromStyle();
void PauseFromStyle();
void CancelFromStyle(PostRestyleMode aPostRestyle) {
Animation::Cancel(aPostRestyle);
// When an animation is disassociated with style it enters an odd state
// where its composite order is undefined until it first transitions
// out of the idle state.
//
// Even if the composite order isn't defined we don't want it to be random
// in case we need to determine the order to dispatch events associated
// with an animation in this state. To solve this we treat the animation as
// if it had been added to the end of the global animation list so that
// its sort order is defined. We'll update this index again once the
// animation leaves the idle state.
//
// Note: We have to update |mAnimationIndex| after calling
// Animation::Cancel(), which enqueues animationcancel event, to make sure
// we have the correct |mAnimationIndex| in AnimationEventInfo.
mAnimationIndex = sNextAnimationIndex++;
mNeedsNewAnimationIndexWhenRun = true;
// We need to do this *after* calling Cancel() since
// Cancel() might synchronously trigger a cancel event for which
// we need an owning element to target the event at.
mOwningElement = OwningElementRef();
}
void Tick(TickState&) override;
void QueueEvents(
const StickyTimeDuration& aActiveTime = StickyTimeDuration());
int32_t CompareCompositeOrder(const CSSAnimation& aOther,
nsContentUtils::NodeIndexCache&) const;
void SetAnimationIndex(uint64_t aIndex) {
MOZ_ASSERT(IsTiedToMarkup());
if (IsRelevant() && mAnimationIndex != aIndex) {
MutationObservers::NotifyAnimationChanged(this);
PostUpdate();
}
mAnimationIndex = aIndex;
}
// Sets the owning element which is used for determining the composite
// order of CSSAnimation objects generated from CSS markup.
//
// @see mOwningElement
void SetOwningElement(const OwningElementRef& aElement) {
mOwningElement = aElement;
}
// True for animations that are generated from CSS markup and continue to
// reflect changes to that markup.
bool IsTiedToMarkup() const { return mOwningElement.IsSet(); }
void MaybeQueueCancelEvent(const StickyTimeDuration& aActiveTime) override {
QueueEvents(aActiveTime);
}
void PropertiesWillSetFromJS(CSSAnimationProperties aProperties) override {
mOverriddenProperties |= aProperties;
}
CSSAnimationProperties PropertiesOverridenByJS() const override {
return mOverriddenProperties;
}
protected:
virtual ~CSSAnimation() {
MOZ_ASSERT(!mOwningElement.IsSet(),
"Owning element should be cleared "
"before a CSS animation is destroyed");
}
// Animation overrides
void UpdateTiming(SeekFlag aSeekFlag,
SyncNotifyFlag aSyncNotifyFlag) override;
// Returns the duration from the start of the animation's source effect's
// active interval to the point where the animation actually begins playback.
// This is zero unless the animation's source effect has a negative delay in
// which case it is the absolute value of that delay.
// This is used for setting the elapsedTime member of CSS AnimationEvents.
TimeDuration InitialAdvance() const {
return mEffect ? std::max(TimeDuration(),
mEffect->NormalizedTiming().Delay() * -1)
: TimeDuration();
}
RefPtr<nsAtom> mAnimationName;
// The (pseudo-)element whose computed animation-name refers to this
// animation (if any).
//
// This is used for determining the relative composite order of animations
// generated from CSS markup.
//
// Typically this will be the same as the target element of the keyframe
// effect associated with this animation. However, it can differ in the
// following circumstances:
//
// a) If script removes or replaces the effect of this animation,
// b) If this animation is cancelled (e.g. by updating the
// animation-name property or removing the owning element from the
// document),
// c) If this object is generated from script using the CSSAnimation
// constructor.
//
// For (b) and (c) the owning element will return !IsSet().
OwningElementRef mOwningElement;
// When true, indicates that when this animation next leaves the idle state,
// its animation index should be updated.
bool mNeedsNewAnimationIndexWhenRun;
// Phase and current iteration from the previous time we queued events.
// This is used to determine what new events to dispatch.
ComputedTiming::AnimationPhase mPreviousPhase;
uint64_t mPreviousIteration;
// Properties that would normally be defined by the cascade but which have
// since been explicitly set via the Web Animations API.
CSSAnimationProperties mOverriddenProperties = CSSAnimationProperties::None;
};
// A subclass of KeyframeEffect that reports when specific properties have been
// overridden via the Web Animations API.
class CSSAnimationKeyframeEffect : public KeyframeEffect {
public:
CSSAnimationKeyframeEffect(Document* aDocument,
OwningAnimationTarget&& aTarget,
TimingParams&& aTiming,
const KeyframeEffectParams& aOptions)
: KeyframeEffect(aDocument, std::move(aTarget), std::move(aTiming),
aOptions) {}
CSSAnimationKeyframeEffect* AsCSSAnimationKeyframeEffect() override {
return this;
}
void GetTiming(EffectTiming& aRetVal) const override;
void GetComputedTimingAsDict(ComputedEffectTiming& aRetVal) const override;
void UpdateTiming(const OptionalEffectTiming& aTiming,
ErrorResult& aRv) override;
void SetKeyframes(JSContext* aContext, JS::Handle<JSObject*> aKeyframes,
ErrorResult& aRv) override;
void SetComposite(const CompositeOperation& aComposite) override;
void SetDefaultTimingFunction(
const StyleComputedTimingFunction& aTimingFunction) {
mDefaultTimingFunction = aTimingFunction;
}
void SetDefaultComposite(const CompositeOperation& aComposite) {
mDefaultComposite = aComposite;
}
bool GetComputedKeyframes(nsTArray<Keyframe>& aKeyframes) const override;
private:
CSSAnimation* GetOwningCSSAnimation() {
return mAnimation ? mAnimation->AsCSSAnimation() : nullptr;
}
const CSSAnimation* GetOwningCSSAnimation() const {
return mAnimation ? mAnimation->AsCSSAnimation() : nullptr;
}
// Flushes styles if our owning animation is a CSSAnimation
void MaybeFlushUnanimatedStyle() const;
// The default timing function is the corresponding computed value of
// animation-timing-function on element.
// Note: We shouldn't reuse |mTiming.mFunction| because it has other usages,
// e.g. it should still be "linear" when calling getTiming() and computing the
// progress.
// https://drafts.csswg.org/css-animations-2/#keyframe-processing
StyleComputedTimingFunction mDefaultTimingFunction =
StyleComputedTimingFunction::Keyword(StyleTimingKeyword::Ease);
// The default composite is the corresponding computed value of
// animation-composition on element.
// Note: We cannot reuse |mEffectOptions.mComposite| which may be updated by
// Web Animations. We should always use the animation-composition from style.
CompositeOperation mDefaultComposite = CompositeOperation::Replace;
// True if we should ignore keyframes generation (for 0% and 100%) because the
// new keyframes are from JS. It's unfortunate we cannot reuse
// |CSSAnimation::mOverriddenProperties| because this keyframe effect may not
// be associated with an animation.
bool mIgnoreKeyframesGeneration = false;
};
} // namespace dom
} // namespace mozilla
#endif // mozilla_dom_CSSAnimation_h