forked from mirrors/gecko-dev
Basically, the fuzzy happens because the direction angle in circle(), ellipse(), and inset() can not be equal to the degree perfectly (due to the arc drawing implementation and the anti-alias around the element), so we need some tolerance. And drop offset-path-shape(-ref).html because it has been removed from the upstream repo. Differential Revision: https://phabricator.services.mozilla.com/D179630
845 lines
34 KiB
C++
845 lines
34 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 "AnimationHelper.h"
|
|
#include "base/process_util.h"
|
|
#include "gfx2DGlue.h" // for ThebesRect
|
|
#include "gfxLineSegment.h" // for gfxLineSegment
|
|
#include "gfxPoint.h" // for gfxPoint
|
|
#include "gfxQuad.h" // for gfxQuad
|
|
#include "gfxRect.h" // for gfxRect
|
|
#include "gfxUtils.h" // for gfxUtils::TransformToQuad
|
|
#include "mozilla/ServoStyleConsts.h" // for StyleComputedTimingFunction
|
|
#include "mozilla/dom/AnimationEffectBinding.h" // for dom::FillMode
|
|
#include "mozilla/dom/KeyframeEffectBinding.h" // for dom::IterationComposite
|
|
#include "mozilla/dom/KeyframeEffect.h" // for dom::KeyFrameEffectReadOnly
|
|
#include "mozilla/dom/Nullable.h" // for dom::Nullable
|
|
#include "mozilla/layers/APZSampler.h" // for APZSampler
|
|
#include "mozilla/layers/CompositorThread.h" // for CompositorThreadHolder
|
|
#include "mozilla/LayerAnimationInfo.h" // for GetCSSPropertiesFor()
|
|
#include "mozilla/Maybe.h" // for Maybe<>
|
|
#include "mozilla/MotionPathUtils.h" // for ResolveMotionPath()
|
|
#include "mozilla/ServoBindings.h" // for Servo_ComposeAnimationSegment, etc
|
|
#include "mozilla/StyleAnimationValue.h" // for StyleAnimationValue, etc
|
|
#include "nsCSSPropertyID.h" // for eCSSProperty_offset_path, etc
|
|
#include "nsDeviceContext.h" // for AppUnitsPerCSSPixel
|
|
#include "nsDisplayList.h" // for nsDisplayTransform, etc
|
|
|
|
namespace mozilla {
|
|
namespace layers {
|
|
|
|
static dom::Nullable<TimeDuration> CalculateElapsedTimeForScrollTimeline(
|
|
const Maybe<APZSampler::ScrollOffsetAndRange> aScrollMeta,
|
|
const ScrollTimelineOptions& aOptions, const StickyTimeDuration& aEndTime,
|
|
const TimeDuration& aStartTime, float aPlaybackRate) {
|
|
// We return Nothing If the associated APZ controller is not available
|
|
// (because it may be destroyed but this animation is still alive).
|
|
if (!aScrollMeta) {
|
|
// This may happen after we reload a page. There may be a race condition
|
|
// because the animation is still alive but the APZ is destroyed. In this
|
|
// case, this animation is invalid, so we return nullptr.
|
|
return nullptr;
|
|
}
|
|
|
|
const bool isHorizontal =
|
|
aOptions.axis() == layers::ScrollDirection::eHorizontal;
|
|
double range =
|
|
isHorizontal ? aScrollMeta->mRange.width : aScrollMeta->mRange.height;
|
|
MOZ_ASSERT(
|
|
range > 0,
|
|
"We don't expect to get a zero or negative range on the compositor");
|
|
|
|
// The offset may be negative if the writing mode is from right to left.
|
|
// Use std::abs() here to avoid getting a negative progress.
|
|
double position =
|
|
std::abs(isHorizontal ? aScrollMeta->mOffset.x : aScrollMeta->mOffset.y);
|
|
double progress = position / range;
|
|
// Just in case to avoid getting a progress more than 100%, for overscrolling.
|
|
progress = std::min(progress, 1.0);
|
|
auto timelineTime = TimeDuration(aEndTime.MultDouble(progress));
|
|
return dom::Animation::CurrentTimeFromTimelineTime(timelineTime, aStartTime,
|
|
aPlaybackRate);
|
|
}
|
|
|
|
static dom::Nullable<TimeDuration> CalculateElapsedTime(
|
|
const APZSampler* aAPZSampler, const LayersId& aLayersId,
|
|
const MutexAutoLock& aProofOfMapLock, const PropertyAnimation& aAnimation,
|
|
const TimeStamp aPreviousFrameTime, const TimeStamp aCurrentFrameTime,
|
|
const AnimatedValue* aPreviousValue) {
|
|
// -------------------------------------
|
|
// Case 1: scroll-timeline animations.
|
|
// -------------------------------------
|
|
if (aAnimation.mScrollTimelineOptions) {
|
|
MOZ_ASSERT(
|
|
aAPZSampler,
|
|
"We don't send scroll animations to the compositor if APZ is disabled");
|
|
|
|
return CalculateElapsedTimeForScrollTimeline(
|
|
aAPZSampler->GetCurrentScrollOffsetAndRange(
|
|
aLayersId, aAnimation.mScrollTimelineOptions.value().source(),
|
|
aProofOfMapLock),
|
|
aAnimation.mScrollTimelineOptions.value(), aAnimation.mTiming.EndTime(),
|
|
aAnimation.mStartTime.refOr(aAnimation.mHoldTime),
|
|
aAnimation.mPlaybackRate);
|
|
}
|
|
|
|
// -------------------------------------
|
|
// Case 2: document-timeline animations.
|
|
// -------------------------------------
|
|
MOZ_ASSERT(
|
|
(!aAnimation.mOriginTime.IsNull() && aAnimation.mStartTime.isSome()) ||
|
|
aAnimation.mIsNotPlaying,
|
|
"If we are playing, we should have an origin time and a start time");
|
|
|
|
// Determine if the animation was play-pending and used a ready time later
|
|
// than the previous frame time.
|
|
//
|
|
// To determine this, _all_ of the following conditions need to hold:
|
|
//
|
|
// * There was no previous animation value (i.e. this is the first frame for
|
|
// the animation since it was sent to the compositor), and
|
|
// * The animation is playing, and
|
|
// * There is a previous frame time, and
|
|
// * The ready time of the animation is ahead of the previous frame time.
|
|
//
|
|
bool hasFutureReadyTime = false;
|
|
if (!aPreviousValue && !aAnimation.mIsNotPlaying &&
|
|
!aPreviousFrameTime.IsNull()) {
|
|
// This is the inverse of the calculation performed in
|
|
// AnimationInfo::StartPendingAnimations to calculate the start time of
|
|
// play-pending animations.
|
|
// Note that we have to calculate (TimeStamp + TimeDuration) last to avoid
|
|
// underflow in the middle of the calulation.
|
|
const TimeStamp readyTime =
|
|
aAnimation.mOriginTime +
|
|
(aAnimation.mStartTime.ref() +
|
|
aAnimation.mHoldTime.MultDouble(1.0 / aAnimation.mPlaybackRate));
|
|
hasFutureReadyTime = !readyTime.IsNull() && readyTime > aPreviousFrameTime;
|
|
}
|
|
// Use the previous vsync time to make main thread animations and compositor
|
|
// more closely aligned.
|
|
//
|
|
// On the first frame where we have animations the previous timestamp will
|
|
// not be set so we simply use the current timestamp. As a result we will
|
|
// end up painting the first frame twice. That doesn't appear to be
|
|
// noticeable, however.
|
|
//
|
|
// Likewise, if the animation is play-pending, it may have a ready time that
|
|
// is *after* |aPreviousFrameTime| (but *before* |aCurrentFrameTime|).
|
|
// To avoid flicker we need to use |aCurrentFrameTime| to avoid temporarily
|
|
// jumping backwards into the range prior to when the animation starts.
|
|
const TimeStamp& timeStamp = aPreviousFrameTime.IsNull() || hasFutureReadyTime
|
|
? aCurrentFrameTime
|
|
: aPreviousFrameTime;
|
|
|
|
// If the animation is not currently playing, e.g. paused or
|
|
// finished, then use the hold time to stay at the same position.
|
|
TimeDuration elapsedDuration =
|
|
aAnimation.mIsNotPlaying || aAnimation.mStartTime.isNothing()
|
|
? aAnimation.mHoldTime
|
|
: (timeStamp - aAnimation.mOriginTime - aAnimation.mStartTime.ref())
|
|
.MultDouble(aAnimation.mPlaybackRate);
|
|
return elapsedDuration;
|
|
}
|
|
|
|
enum class CanSkipCompose {
|
|
IfPossible,
|
|
No,
|
|
};
|
|
// This function samples the animation for a specific property. We may have
|
|
// multiple animations for a single property, and the later animations override
|
|
// the eariler ones. This function returns the sampled animation value,
|
|
// |aAnimationValue| for a single CSS property.
|
|
static AnimationHelper::SampleResult SampleAnimationForProperty(
|
|
const APZSampler* aAPZSampler, const LayersId& aLayersId,
|
|
const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
|
|
TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
|
|
CanSkipCompose aCanSkipCompose,
|
|
nsTArray<PropertyAnimation>& aPropertyAnimations,
|
|
RefPtr<StyleAnimationValue>& aAnimationValue) {
|
|
MOZ_ASSERT(!aPropertyAnimations.IsEmpty(), "Should have animations");
|
|
|
|
auto reason = AnimationHelper::SampleResult::Reason::None;
|
|
bool hasInEffectAnimations = false;
|
|
#ifdef DEBUG
|
|
// In cases where this function returns a SampleResult::Skipped, we actually
|
|
// do populate aAnimationValue in debug mode, so that we can MOZ_ASSERT at the
|
|
// call site that the value that would have been computed matches the stored
|
|
// value that we end up using. This flag is used to ensure we populate
|
|
// aAnimationValue in this scenario.
|
|
bool shouldBeSkipped = false;
|
|
#endif
|
|
// Process in order, since later animations override earlier ones.
|
|
for (PropertyAnimation& animation : aPropertyAnimations) {
|
|
dom::Nullable<TimeDuration> elapsedDuration = CalculateElapsedTime(
|
|
aAPZSampler, aLayersId, aProofOfMapLock, animation, aPreviousFrameTime,
|
|
aCurrentFrameTime, aPreviousValue);
|
|
|
|
const auto progressTimelinePosition =
|
|
animation.mScrollTimelineOptions
|
|
? dom::Animation::AtProgressTimelineBoundary(
|
|
TimeDuration::FromMilliseconds(
|
|
PROGRESS_TIMELINE_DURATION_MILLISEC),
|
|
elapsedDuration, animation.mStartTime.refOr(TimeDuration()),
|
|
animation.mPlaybackRate)
|
|
: dom::Animation::ProgressTimelinePosition::NotBoundary;
|
|
|
|
ComputedTiming computedTiming = dom::AnimationEffect::GetComputedTimingAt(
|
|
elapsedDuration, animation.mTiming, animation.mPlaybackRate,
|
|
progressTimelinePosition);
|
|
|
|
if (computedTiming.mProgress.IsNull()) {
|
|
// For the scroll-driven animations, it's possible to let it go between
|
|
// the active phase and the before/after phase, and so its progress
|
|
// becomes null. In this case, we shouldn't just skip this animation.
|
|
// Instead, we have to reset the previous sampled result. Basically, we
|
|
// use |mProgressOnLastCompose| to check if it goes from the active phase.
|
|
// If so, we set the returned |mReason| to ScrollToDelayPhase to let the
|
|
// caller know we need to use the base style for this property.
|
|
//
|
|
// If there are any other animations which need to be sampled together
|
|
// (in the same property animation group), this |reason| will be ignored.
|
|
if (animation.mScrollTimelineOptions &&
|
|
!animation.mProgressOnLastCompose.IsNull() &&
|
|
(computedTiming.mPhase == ComputedTiming::AnimationPhase::Before ||
|
|
computedTiming.mPhase == ComputedTiming::AnimationPhase::After)) {
|
|
// Appearally, we go back to delay, so need to reset the last
|
|
// composition meta data. This is necessary because
|
|
// 1. this animation is in delay so it shouldn't have any composition
|
|
// meta data, and
|
|
// 2. we will not go into this condition multiple times during delay
|
|
// phase because we rely on |mProgressOnLastCompose|.
|
|
animation.ResetLastCompositionValues();
|
|
reason = AnimationHelper::SampleResult::Reason::ScrollToDelayPhase;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
dom::IterationCompositeOperation iterCompositeOperation =
|
|
animation.mIterationComposite;
|
|
|
|
// Skip calculation if the progress hasn't changed since the last
|
|
// calculation.
|
|
// Note that we don't skip calculate this animation if there is another
|
|
// animation since the other animation might be 'accumulate' or 'add', or
|
|
// might have a missing keyframe (i.e. this animation value will be used in
|
|
// the missing keyframe).
|
|
// FIXME Bug 1455476: We should do this optimizations for the case where
|
|
// the layer has multiple animations and multiple properties.
|
|
if (aCanSkipCompose == CanSkipCompose::IfPossible &&
|
|
!dom::KeyframeEffect::HasComputedTimingChanged(
|
|
computedTiming, iterCompositeOperation,
|
|
animation.mProgressOnLastCompose,
|
|
animation.mCurrentIterationOnLastCompose)) {
|
|
#ifdef DEBUG
|
|
shouldBeSkipped = true;
|
|
#else
|
|
return AnimationHelper::SampleResult::Skipped();
|
|
#endif
|
|
}
|
|
|
|
uint32_t segmentIndex = 0;
|
|
size_t segmentSize = animation.mSegments.Length();
|
|
PropertyAnimation::SegmentData* segment = animation.mSegments.Elements();
|
|
while (segment->mEndPortion < computedTiming.mProgress.Value() &&
|
|
segmentIndex < segmentSize - 1) {
|
|
++segment;
|
|
++segmentIndex;
|
|
}
|
|
|
|
double positionInSegment =
|
|
(computedTiming.mProgress.Value() - segment->mStartPortion) /
|
|
(segment->mEndPortion - segment->mStartPortion);
|
|
|
|
double portion = StyleComputedTimingFunction::GetPortion(
|
|
segment->mFunction, positionInSegment, computedTiming.mBeforeFlag);
|
|
|
|
// Like above optimization, skip calculation if the target segment isn't
|
|
// changed and if the portion in the segment isn't changed.
|
|
// This optimization is needed for CSS animations/transitions with step
|
|
// timing functions (e.g. the throbber animation on tabs or frame based
|
|
// animations).
|
|
// FIXME Bug 1455476: Like the above optimization, we should apply this
|
|
// optimizations for multiple animation cases and multiple properties as
|
|
// well.
|
|
if (aCanSkipCompose == CanSkipCompose::IfPossible &&
|
|
animation.mSegmentIndexOnLastCompose == segmentIndex &&
|
|
!animation.mPortionInSegmentOnLastCompose.IsNull() &&
|
|
animation.mPortionInSegmentOnLastCompose.Value() == portion) {
|
|
#ifdef DEBUG
|
|
shouldBeSkipped = true;
|
|
#else
|
|
return AnimationHelper::SampleResult::Skipped();
|
|
#endif
|
|
}
|
|
|
|
AnimationPropertySegment animSegment;
|
|
animSegment.mFromKey = 0.0;
|
|
animSegment.mToKey = 1.0;
|
|
animSegment.mFromValue = AnimationValue(segment->mStartValue);
|
|
animSegment.mToValue = AnimationValue(segment->mEndValue);
|
|
animSegment.mFromComposite = segment->mStartComposite;
|
|
animSegment.mToComposite = segment->mEndComposite;
|
|
|
|
// interpolate the property
|
|
aAnimationValue =
|
|
Servo_ComposeAnimationSegment(
|
|
&animSegment, aAnimationValue,
|
|
animation.mSegments.LastElement().mEndValue, iterCompositeOperation,
|
|
portion, computedTiming.mCurrentIteration)
|
|
.Consume();
|
|
|
|
#ifdef DEBUG
|
|
if (shouldBeSkipped) {
|
|
return AnimationHelper::SampleResult::Skipped();
|
|
}
|
|
#endif
|
|
|
|
hasInEffectAnimations = true;
|
|
animation.mProgressOnLastCompose = computedTiming.mProgress;
|
|
animation.mCurrentIterationOnLastCompose = computedTiming.mCurrentIteration;
|
|
animation.mSegmentIndexOnLastCompose = segmentIndex;
|
|
animation.mPortionInSegmentOnLastCompose.SetValue(portion);
|
|
}
|
|
|
|
auto rv = hasInEffectAnimations ? AnimationHelper::SampleResult::Sampled()
|
|
: AnimationHelper::SampleResult();
|
|
rv.mReason = reason;
|
|
return rv;
|
|
}
|
|
|
|
// This function samples the animations for a group of CSS properties. We may
|
|
// have multiple CSS properties in a group (e.g. transform-like properties).
|
|
// So the returned animation array, |aAnimationValues|, include all the
|
|
// animation values of these CSS properties.
|
|
AnimationHelper::SampleResult AnimationHelper::SampleAnimationForEachNode(
|
|
const APZSampler* aAPZSampler, const LayersId& aLayersId,
|
|
const MutexAutoLock& aProofOfMapLock, TimeStamp aPreviousFrameTime,
|
|
TimeStamp aCurrentFrameTime, const AnimatedValue* aPreviousValue,
|
|
nsTArray<PropertyAnimationGroup>& aPropertyAnimationGroups,
|
|
nsTArray<RefPtr<StyleAnimationValue>>& aAnimationValues /* out */) {
|
|
MOZ_ASSERT(!aPropertyAnimationGroups.IsEmpty(),
|
|
"Should be called with animation data");
|
|
MOZ_ASSERT(aAnimationValues.IsEmpty(),
|
|
"Should be called with empty aAnimationValues");
|
|
|
|
nsTArray<RefPtr<StyleAnimationValue>> baseStyleOfDelayAnimations;
|
|
nsTArray<RefPtr<StyleAnimationValue>> nonAnimatingValues;
|
|
for (PropertyAnimationGroup& group : aPropertyAnimationGroups) {
|
|
// Initialize animation value with base style.
|
|
RefPtr<StyleAnimationValue> currValue = group.mBaseStyle;
|
|
|
|
CanSkipCompose canSkipCompose =
|
|
aPreviousValue && aPropertyAnimationGroups.Length() == 1 &&
|
|
group.mAnimations.Length() == 1
|
|
? CanSkipCompose::IfPossible
|
|
: CanSkipCompose::No;
|
|
|
|
MOZ_ASSERT(
|
|
!group.mAnimations.IsEmpty() ||
|
|
nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
|
|
group.mProperty),
|
|
"Only transform-like properties can have empty PropertyAnimation list");
|
|
|
|
// For properties which are not animating (i.e. their values are always the
|
|
// same), we store them in a different array, and then merge them into the
|
|
// final result (a.k.a. aAnimationValues) because we shouldn't take them
|
|
// into account for SampleResult. (In other words, these properties
|
|
// shouldn't affect the optimization.)
|
|
if (group.mAnimations.IsEmpty()) {
|
|
nonAnimatingValues.AppendElement(std::move(currValue));
|
|
continue;
|
|
}
|
|
|
|
SampleResult result = SampleAnimationForProperty(
|
|
aAPZSampler, aLayersId, aProofOfMapLock, aPreviousFrameTime,
|
|
aCurrentFrameTime, aPreviousValue, canSkipCompose, group.mAnimations,
|
|
currValue);
|
|
|
|
// FIXME: Bug 1455476: Do optimization for multiple properties. For now,
|
|
// the result is skipped only if the property count == 1.
|
|
if (result.IsSkipped()) {
|
|
#ifdef DEBUG
|
|
aAnimationValues.AppendElement(std::move(currValue));
|
|
#endif
|
|
return result;
|
|
}
|
|
|
|
if (!result.IsSampled()) {
|
|
if (result.mReason == SampleResult::Reason::ScrollToDelayPhase) {
|
|
MOZ_ASSERT(currValue && currValue == group.mBaseStyle);
|
|
baseStyleOfDelayAnimations.AppendElement(std::move(currValue));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Insert the interpolation result into the output array.
|
|
MOZ_ASSERT(currValue);
|
|
aAnimationValues.AppendElement(std::move(currValue));
|
|
}
|
|
|
|
SampleResult rv =
|
|
aAnimationValues.IsEmpty() ? SampleResult() : SampleResult::Sampled();
|
|
|
|
// If there is no other sampled result, we may store these base styles
|
|
// (together with the non-animating values) to the webrenderer before it gets
|
|
// sync with the main thread.
|
|
if (rv.IsNone() && !baseStyleOfDelayAnimations.IsEmpty()) {
|
|
aAnimationValues.AppendElements(std::move(baseStyleOfDelayAnimations));
|
|
rv.mReason = SampleResult::Reason::ScrollToDelayPhase;
|
|
}
|
|
|
|
if (!aAnimationValues.IsEmpty()) {
|
|
aAnimationValues.AppendElements(std::move(nonAnimatingValues));
|
|
}
|
|
return rv;
|
|
}
|
|
|
|
static dom::FillMode GetAdjustedFillMode(const Animation& aAnimation) {
|
|
// Adjust fill mode so that if the main thread is delayed in clearing
|
|
// this animation we don't introduce flicker by jumping back to the old
|
|
// underlying value.
|
|
auto fillMode = static_cast<dom::FillMode>(aAnimation.fillMode());
|
|
float playbackRate = aAnimation.playbackRate();
|
|
switch (fillMode) {
|
|
case dom::FillMode::None:
|
|
if (playbackRate > 0) {
|
|
fillMode = dom::FillMode::Forwards;
|
|
} else if (playbackRate < 0) {
|
|
fillMode = dom::FillMode::Backwards;
|
|
}
|
|
break;
|
|
case dom::FillMode::Backwards:
|
|
if (playbackRate > 0) {
|
|
fillMode = dom::FillMode::Both;
|
|
}
|
|
break;
|
|
case dom::FillMode::Forwards:
|
|
if (playbackRate < 0) {
|
|
fillMode = dom::FillMode::Both;
|
|
}
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
return fillMode;
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
static bool HasTransformLikeAnimations(const AnimationArray& aAnimations) {
|
|
nsCSSPropertyIDSet transformSet =
|
|
nsCSSPropertyIDSet::TransformLikeProperties();
|
|
|
|
for (const Animation& animation : aAnimations) {
|
|
if (animation.isNotAnimating()) {
|
|
continue;
|
|
}
|
|
|
|
if (transformSet.HasProperty(animation.property())) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
#endif
|
|
|
|
AnimationStorageData AnimationHelper::ExtractAnimations(
|
|
const LayersId& aLayersId, const AnimationArray& aAnimations) {
|
|
AnimationStorageData storageData;
|
|
storageData.mLayersId = aLayersId;
|
|
|
|
nsCSSPropertyID prevID = eCSSProperty_UNKNOWN;
|
|
PropertyAnimationGroup* currData = nullptr;
|
|
DebugOnly<const layers::Animatable*> currBaseStyle = nullptr;
|
|
|
|
for (const Animation& animation : aAnimations) {
|
|
// Animations with same property are grouped together, so we can just
|
|
// check if the current property is the same as the previous one for
|
|
// knowing this is a new group.
|
|
if (prevID != animation.property()) {
|
|
// Got a different group, we should create a different array.
|
|
currData = storageData.mAnimation.AppendElement();
|
|
currData->mProperty = animation.property();
|
|
if (animation.transformData()) {
|
|
MOZ_ASSERT(!storageData.mTransformData,
|
|
"Only one entry has TransformData");
|
|
storageData.mTransformData = animation.transformData();
|
|
}
|
|
|
|
prevID = animation.property();
|
|
|
|
// Reset the debug pointer.
|
|
currBaseStyle = nullptr;
|
|
}
|
|
|
|
MOZ_ASSERT(currData);
|
|
if (animation.baseStyle().type() != Animatable::Tnull_t) {
|
|
MOZ_ASSERT(!currBaseStyle || *currBaseStyle == animation.baseStyle(),
|
|
"Should be the same base style");
|
|
|
|
currData->mBaseStyle = AnimationValue::FromAnimatable(
|
|
animation.property(), animation.baseStyle());
|
|
currBaseStyle = &animation.baseStyle();
|
|
}
|
|
|
|
// If this layers::Animation sets isNotAnimating to true, it only has
|
|
// base style and doesn't have any animation information, so we can skip
|
|
// the rest steps. (And so its PropertyAnimationGroup::mAnimation will be
|
|
// an empty array.)
|
|
if (animation.isNotAnimating()) {
|
|
MOZ_ASSERT(nsCSSPropertyIDSet::TransformLikeProperties().HasProperty(
|
|
animation.property()),
|
|
"Only transform-like properties could set this true");
|
|
|
|
if (animation.property() == eCSSProperty_offset_path) {
|
|
MOZ_ASSERT(currData->mBaseStyle,
|
|
"Fixed offset-path should have base style");
|
|
MOZ_ASSERT(HasTransformLikeAnimations(aAnimations));
|
|
|
|
const StyleOffsetPath& offsetPath =
|
|
animation.baseStyle().get_StyleOffsetPath();
|
|
// FIXME: Bug 1837042. Cache all basic shapes.
|
|
if (offsetPath.IsPath()) {
|
|
MOZ_ASSERT(!storageData.mCachedMotionPath,
|
|
"Only one offset-path: path() is set");
|
|
|
|
RefPtr<gfx::PathBuilder> builder =
|
|
MotionPathUtils::GetCompositorPathBuilder();
|
|
storageData.mCachedMotionPath = MotionPathUtils::BuildSVGPath(
|
|
offsetPath.AsSVGPathData(), builder);
|
|
}
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
PropertyAnimation* propertyAnimation =
|
|
currData->mAnimations.AppendElement();
|
|
|
|
propertyAnimation->mOriginTime = animation.originTime();
|
|
propertyAnimation->mStartTime = animation.startTime();
|
|
propertyAnimation->mHoldTime = animation.holdTime();
|
|
propertyAnimation->mPlaybackRate = animation.playbackRate();
|
|
propertyAnimation->mIterationComposite =
|
|
static_cast<dom::IterationCompositeOperation>(
|
|
animation.iterationComposite());
|
|
propertyAnimation->mIsNotPlaying = animation.isNotPlaying();
|
|
propertyAnimation->mTiming =
|
|
TimingParams{animation.duration(),
|
|
animation.delay(),
|
|
animation.endDelay(),
|
|
animation.iterations(),
|
|
animation.iterationStart(),
|
|
static_cast<dom::PlaybackDirection>(animation.direction()),
|
|
GetAdjustedFillMode(animation),
|
|
animation.easingFunction()};
|
|
propertyAnimation->mScrollTimelineOptions =
|
|
animation.scrollTimelineOptions();
|
|
|
|
nsTArray<PropertyAnimation::SegmentData>& segmentData =
|
|
propertyAnimation->mSegments;
|
|
for (const AnimationSegment& segment : animation.segments()) {
|
|
segmentData.AppendElement(PropertyAnimation::SegmentData{
|
|
AnimationValue::FromAnimatable(animation.property(),
|
|
segment.startState()),
|
|
AnimationValue::FromAnimatable(animation.property(),
|
|
segment.endState()),
|
|
segment.sampleFn(), segment.startPortion(), segment.endPortion(),
|
|
static_cast<dom::CompositeOperation>(segment.startComposite()),
|
|
static_cast<dom::CompositeOperation>(segment.endComposite())});
|
|
}
|
|
}
|
|
|
|
#ifdef DEBUG
|
|
// Sanity check that the grouped animation data is correct by looking at the
|
|
// property set.
|
|
if (!storageData.mAnimation.IsEmpty()) {
|
|
nsCSSPropertyIDSet seenProperties;
|
|
for (const auto& group : storageData.mAnimation) {
|
|
nsCSSPropertyID id = group.mProperty;
|
|
|
|
MOZ_ASSERT(!seenProperties.HasProperty(id), "Should be a new property");
|
|
seenProperties.AddProperty(id);
|
|
}
|
|
|
|
MOZ_ASSERT(
|
|
seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
|
|
DisplayItemType::TYPE_TRANSFORM)) ||
|
|
seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
|
|
DisplayItemType::TYPE_OPACITY)) ||
|
|
seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
|
|
DisplayItemType::TYPE_BACKGROUND_COLOR)),
|
|
"The property set of output should be the subset of transform-like "
|
|
"properties, opacity, or background_color.");
|
|
|
|
if (seenProperties.IsSubsetOf(LayerAnimationInfo::GetCSSPropertiesFor(
|
|
DisplayItemType::TYPE_TRANSFORM))) {
|
|
MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
|
|
}
|
|
|
|
if (seenProperties.HasProperty(eCSSProperty_offset_path)) {
|
|
MOZ_ASSERT(storageData.mTransformData, "Should have TransformData");
|
|
MOZ_ASSERT(storageData.mTransformData->motionPathData(),
|
|
"Should have MotionPathData");
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return storageData;
|
|
}
|
|
|
|
uint64_t AnimationHelper::GetNextCompositorAnimationsId() {
|
|
static uint32_t sNextId = 0;
|
|
++sNextId;
|
|
|
|
uint32_t procId = static_cast<uint32_t>(base::GetCurrentProcId());
|
|
uint64_t nextId = procId;
|
|
nextId = nextId << 32 | sNextId;
|
|
return nextId;
|
|
}
|
|
|
|
gfx::Matrix4x4 AnimationHelper::ServoAnimationValueToMatrix4x4(
|
|
const nsTArray<RefPtr<StyleAnimationValue>>& aValues,
|
|
const TransformData& aTransformData, gfx::Path* aCachedMotionPath) {
|
|
using nsStyleTransformMatrix::TransformReferenceBox;
|
|
|
|
// This is a bit silly just to avoid the transform list copy from the
|
|
// animation transform list.
|
|
auto noneTranslate = StyleTranslate::None();
|
|
auto noneRotate = StyleRotate::None();
|
|
auto noneScale = StyleScale::None();
|
|
const StyleTransform noneTransform;
|
|
|
|
const StyleTranslate* translate = nullptr;
|
|
const StyleRotate* rotate = nullptr;
|
|
const StyleScale* scale = nullptr;
|
|
const StyleTransform* transform = nullptr;
|
|
Maybe<StyleOffsetPath> path;
|
|
const StyleLengthPercentage* distance = nullptr;
|
|
const StyleOffsetRotate* offsetRotate = nullptr;
|
|
const StylePositionOrAuto* anchor = nullptr;
|
|
const StyleOffsetPosition* position = nullptr;
|
|
|
|
for (const auto& value : aValues) {
|
|
MOZ_ASSERT(value);
|
|
nsCSSPropertyID id = Servo_AnimationValue_GetPropertyId(value);
|
|
switch (id) {
|
|
case eCSSProperty_transform:
|
|
MOZ_ASSERT(!transform);
|
|
transform = Servo_AnimationValue_GetTransform(value);
|
|
break;
|
|
case eCSSProperty_translate:
|
|
MOZ_ASSERT(!translate);
|
|
translate = Servo_AnimationValue_GetTranslate(value);
|
|
break;
|
|
case eCSSProperty_rotate:
|
|
MOZ_ASSERT(!rotate);
|
|
rotate = Servo_AnimationValue_GetRotate(value);
|
|
break;
|
|
case eCSSProperty_scale:
|
|
MOZ_ASSERT(!scale);
|
|
scale = Servo_AnimationValue_GetScale(value);
|
|
break;
|
|
case eCSSProperty_offset_path:
|
|
MOZ_ASSERT(!path);
|
|
path.emplace(StyleOffsetPath::None());
|
|
Servo_AnimationValue_GetOffsetPath(value, path.ptr());
|
|
break;
|
|
case eCSSProperty_offset_distance:
|
|
MOZ_ASSERT(!distance);
|
|
distance = Servo_AnimationValue_GetOffsetDistance(value);
|
|
break;
|
|
case eCSSProperty_offset_rotate:
|
|
MOZ_ASSERT(!offsetRotate);
|
|
offsetRotate = Servo_AnimationValue_GetOffsetRotate(value);
|
|
break;
|
|
case eCSSProperty_offset_anchor:
|
|
MOZ_ASSERT(!anchor);
|
|
anchor = Servo_AnimationValue_GetOffsetAnchor(value);
|
|
break;
|
|
case eCSSProperty_offset_position:
|
|
MOZ_ASSERT(!position);
|
|
position = Servo_AnimationValue_GetOffsetPosition(value);
|
|
break;
|
|
default:
|
|
MOZ_ASSERT_UNREACHABLE("Unsupported transform-like property");
|
|
}
|
|
}
|
|
|
|
TransformReferenceBox refBox(nullptr, aTransformData.bounds());
|
|
Maybe<ResolvedMotionPathData> motion = MotionPathUtils::ResolveMotionPath(
|
|
path.ptrOr(nullptr), distance, offsetRotate, anchor, position,
|
|
aTransformData.motionPathData(), refBox, aCachedMotionPath);
|
|
|
|
// We expect all our transform data to arrive in device pixels
|
|
gfx::Point3D transformOrigin = aTransformData.transformOrigin();
|
|
nsDisplayTransform::FrameTransformProperties props(
|
|
translate ? *translate : noneTranslate, rotate ? *rotate : noneRotate,
|
|
scale ? *scale : noneScale, transform ? *transform : noneTransform,
|
|
motion, transformOrigin);
|
|
|
|
return nsDisplayTransform::GetResultingTransformMatrix(
|
|
props, refBox, aTransformData.appUnitsPerDevPixel());
|
|
}
|
|
|
|
static uint8_t CollectOverflowedSideLines(const gfxQuad& aPrerenderedQuad,
|
|
SideBits aOverflowSides,
|
|
gfxLineSegment sideLines[4]) {
|
|
uint8_t count = 0;
|
|
|
|
if (aOverflowSides & SideBits::eTop) {
|
|
sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[0],
|
|
aPrerenderedQuad.mPoints[1]);
|
|
count++;
|
|
}
|
|
if (aOverflowSides & SideBits::eRight) {
|
|
sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[1],
|
|
aPrerenderedQuad.mPoints[2]);
|
|
count++;
|
|
}
|
|
if (aOverflowSides & SideBits::eBottom) {
|
|
sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[2],
|
|
aPrerenderedQuad.mPoints[3]);
|
|
count++;
|
|
}
|
|
if (aOverflowSides & SideBits::eLeft) {
|
|
sideLines[count] = gfxLineSegment(aPrerenderedQuad.mPoints[3],
|
|
aPrerenderedQuad.mPoints[0]);
|
|
count++;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
enum RegionBits : uint8_t {
|
|
Inside = 0,
|
|
Left = (1 << 0),
|
|
Right = (1 << 1),
|
|
Bottom = (1 << 2),
|
|
Top = (1 << 3),
|
|
};
|
|
|
|
MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RegionBits);
|
|
|
|
static RegionBits GetRegionBitsForPoint(double aX, double aY,
|
|
const gfxRect& aClip) {
|
|
RegionBits result = RegionBits::Inside;
|
|
if (aX < aClip.X()) {
|
|
result |= RegionBits::Left;
|
|
} else if (aX > aClip.XMost()) {
|
|
result |= RegionBits::Right;
|
|
}
|
|
|
|
if (aY < aClip.Y()) {
|
|
result |= RegionBits::Bottom;
|
|
} else if (aY > aClip.YMost()) {
|
|
result |= RegionBits::Top;
|
|
}
|
|
return result;
|
|
};
|
|
|
|
// https://en.wikipedia.org/wiki/Cohen%E2%80%93Sutherland_algorithm
|
|
static bool LineSegmentIntersectsClip(double aX0, double aY0, double aX1,
|
|
double aY1, const gfxRect& aClip) {
|
|
RegionBits b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
|
|
RegionBits b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
|
|
|
|
while (true) {
|
|
if (!(b0 | b1)) {
|
|
// Completely inside.
|
|
return true;
|
|
}
|
|
|
|
if (b0 & b1) {
|
|
// Completely outside.
|
|
return false;
|
|
}
|
|
|
|
double x, y;
|
|
// Choose an outside point.
|
|
RegionBits outsidePointBits = b1 > b0 ? b1 : b0;
|
|
if (outsidePointBits & RegionBits::Top) {
|
|
x = aX0 + (aX1 - aX0) * (aClip.YMost() - aY0) / (aY1 - aY0);
|
|
y = aClip.YMost();
|
|
} else if (outsidePointBits & RegionBits::Bottom) {
|
|
x = aX0 + (aX1 - aX0) * (aClip.Y() - aY0) / (aY1 - aY0);
|
|
y = aClip.Y();
|
|
} else if (outsidePointBits & RegionBits::Right) {
|
|
y = aY0 + (aY1 - aY0) * (aClip.XMost() - aX0) / (aX1 - aX0);
|
|
x = aClip.XMost();
|
|
} else if (outsidePointBits & RegionBits::Left) {
|
|
y = aY0 + (aY1 - aY0) * (aClip.X() - aX0) / (aX1 - aX0);
|
|
x = aClip.X();
|
|
}
|
|
|
|
if (outsidePointBits == b0) {
|
|
aX0 = x;
|
|
aY0 = y;
|
|
b0 = GetRegionBitsForPoint(aX0, aY0, aClip);
|
|
} else {
|
|
aX1 = x;
|
|
aY1 = y;
|
|
b1 = GetRegionBitsForPoint(aX1, aY1, aClip);
|
|
}
|
|
}
|
|
MOZ_ASSERT_UNREACHABLE();
|
|
return false;
|
|
}
|
|
|
|
// static
|
|
bool AnimationHelper::ShouldBeJank(const LayoutDeviceRect& aPrerenderedRect,
|
|
SideBits aOverflowSides,
|
|
const gfx::Matrix4x4& aTransform,
|
|
const ParentLayerRect& aClipRect) {
|
|
if (aClipRect.IsEmpty()) {
|
|
return false;
|
|
}
|
|
|
|
gfxQuad prerenderedQuad = gfxUtils::TransformToQuad(
|
|
ThebesRect(aPrerenderedRect.ToUnknownRect()), aTransform);
|
|
|
|
gfxLineSegment sideLines[4];
|
|
uint8_t overflowSideCount =
|
|
CollectOverflowedSideLines(prerenderedQuad, aOverflowSides, sideLines);
|
|
|
|
gfxRect clipRect = ThebesRect(aClipRect.ToUnknownRect());
|
|
for (uint8_t j = 0; j < overflowSideCount; j++) {
|
|
if (LineSegmentIntersectsClip(sideLines[j].mStart.x, sideLines[j].mStart.y,
|
|
sideLines[j].mEnd.x, sideLines[j].mEnd.y,
|
|
clipRect)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// With step timing functions there are cases the transform jumps to a
|
|
// position where the partial pre-render area is totally outside of the clip
|
|
// rect without any intersection of the partial pre-render area and the clip
|
|
// rect happened in previous compositions but there remains visible area of
|
|
// the entire transformed area.
|
|
//
|
|
// So now all four points of the transformed partial pre-render rect are
|
|
// outside of the clip rect, if all these four points are in either side of
|
|
// the clip rect, we consider it's jank so that on the main-thread we will
|
|
// either a) rebuild the up-to-date display item if there remains visible area
|
|
// or b) no longer rebuild the display item if it's totally outside of the
|
|
// clip rect.
|
|
//
|
|
// Note that RegionBits::Left and Right are mutually exclusive,
|
|
// RegionBits::Top and Bottom are also mutually exclusive, so if there remains
|
|
// any bits, it means all four points are in the same side.
|
|
return GetRegionBitsForPoint(prerenderedQuad.mPoints[0].x,
|
|
prerenderedQuad.mPoints[0].y, clipRect) &
|
|
GetRegionBitsForPoint(prerenderedQuad.mPoints[1].x,
|
|
prerenderedQuad.mPoints[1].y, clipRect) &
|
|
GetRegionBitsForPoint(prerenderedQuad.mPoints[2].x,
|
|
prerenderedQuad.mPoints[2].y, clipRect) &
|
|
GetRegionBitsForPoint(prerenderedQuad.mPoints[3].x,
|
|
prerenderedQuad.mPoints[3].y, clipRect);
|
|
}
|
|
|
|
} // namespace layers
|
|
} // namespace mozilla
|