diff --git a/dom/animation/KeyframeEffect.cpp b/dom/animation/KeyframeEffect.cpp index 198e10fe3d6e..7450ce9b23fe 100644 --- a/dom/animation/KeyframeEffect.cpp +++ b/dom/animation/KeyframeEffect.cpp @@ -475,7 +475,7 @@ KeyframeEffectReadOnly::SetKeyframes(nsTArray&& aKeyframes, // 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::ApplySpacing(mKeyframes, SpacingMode::distribute); + KeyframeUtils::ApplyDistributeSpacing(mKeyframes); if (mAnimation && mAnimation->IsRelevant()) { nsNodeUtils::AnimationChanged(mAnimation); @@ -526,6 +526,12 @@ KeyframeEffectReadOnly::UpdateProperties(nsStyleContext* aStyleContext) KeyframeUtils::GetComputedKeyframeValues(mKeyframes, mTarget->mElement, aStyleContext); + if (mEffectOptions.mSpacingMode == SpacingMode::paced) { + KeyframeUtils::ApplySpacing(mKeyframes, SpacingMode::paced, + mEffectOptions.mPacedProperty, + computedValues); + } + properties = KeyframeUtils::GetAnimationPropertiesFromKeyframes(mKeyframes, computedValues, diff --git a/dom/animation/KeyframeUtils.cpp b/dom/animation/KeyframeUtils.cpp index 76b997798e0b..308137fb0381 100644 --- a/dom/animation/KeyframeUtils.cpp +++ b/dom/animation/KeyframeUtils.cpp @@ -30,6 +30,10 @@ namespace mozilla { // // ------------------------------------------------------------------ +// This is used while calculating paced spacing. If the keyframe is not pacable, +// we set its cumulative distance to kNotPaceable, so we can use this to check. +const double kNotPaceable = -1.0; + // For the aAllowList parameter of AppendStringOrStringSequence and // GetPropertyValuesPairs. enum class ListAllowance { eDisallow, eAllow }; @@ -392,7 +396,19 @@ RequiresAdditiveAnimation(const nsTArray& aKeyframes, nsIDocument* aDocument); static void -DistributeRange(const Range& aKeyframes); +DistributeRange(const Range& aSpacingRange, + const Range& aRangeToAdjust); + +static void +DistributeRange(const Range& aSpacingRange); + +static void +PaceRange(const Range& aKeyframes, + const Range& aCumulativeDistances); + +static nsTArray +GetCumulativeDistances(const nsTArray& aValues, + nsCSSProperty aProperty); // ------------------------------------------------------------------ // @@ -460,12 +476,26 @@ KeyframeUtils::GetKeyframesFromObject(JSContext* aCx, /* static */ void KeyframeUtils::ApplySpacing(nsTArray& aKeyframes, - SpacingMode aSpacingMode) + SpacingMode aSpacingMode, + nsCSSProperty aProperty, + nsTArray& aComputedValues) { if (aKeyframes.IsEmpty()) { return; } + nsTArray cumulativeDistances; + if (aSpacingMode == SpacingMode::paced) { + MOZ_ASSERT(IsAnimatableProperty(aProperty), + "Paced property should be animatable"); + + cumulativeDistances = GetCumulativeDistances(aComputedValues, aProperty); + // Reset the computed offsets if using paced spacing. + for (Keyframe& keyframe : aKeyframes) { + keyframe.mComputedOffset = Keyframe::kComputedOffsetNotSet; + } + } + // If the first keyframe has an unspecified offset, fill it in with 0%. // If there is only a single keyframe, then it gets 100%. if (aKeyframes.Length() > 1) { @@ -479,28 +509,87 @@ KeyframeUtils::ApplySpacing(nsTArray& aKeyframes, // Fill in remaining missing offsets. const Keyframe* const last = aKeyframes.cend() - 1; - RangedPtr keyframeA(aKeyframes.begin(), aKeyframes.Length()); + const RangedPtr begin(aKeyframes.begin(), aKeyframes.Length()); + RangedPtr keyframeA = begin; while (keyframeA != last) { // Find keyframe A and keyframe B *between* which we will apply spacing. RangedPtr keyframeB = keyframeA + 1; - while (keyframeB.get()->mOffset.isNothing() && keyframeB != last) { + while (keyframeB->mOffset.isNothing() && keyframeB != last) { ++keyframeB; } - keyframeB.get()->mComputedOffset = keyframeB.get()->mOffset.valueOr(1.0); + keyframeB->mComputedOffset = keyframeB->mOffset.valueOr(1.0); // Fill computed offsets in (keyframe A, keyframe B). if (aSpacingMode == SpacingMode::distribute) { + // Bug 1276573: Use the new constructor accepting two RangedPtr + // arguments, so we can make the code simpler. DistributeRange(Range(keyframeA.get(), keyframeB - keyframeA + 1)); } else { - // TODO - MOZ_ASSERT(false, "not implement yet"); - } + // a) Find Paced A (first paceable keyframe) and + // Paced B (last paceable keyframe) in [keyframe A, keyframe B]. + RangedPtr pacedA = keyframeA; + while (pacedA < keyframeB && + cumulativeDistances[pacedA - begin] == kNotPaceable) { + ++pacedA; + } + RangedPtr pacedB = keyframeB; + while (pacedB > keyframeA && + cumulativeDistances[pacedB - begin] == kNotPaceable) { + --pacedB; + } + // As spec says, if there is no paceable keyframe + // in [keyframe A, keyframe B], we let Paced A and Paced B refer to + // keyframe B. + if (pacedA > pacedB) { + pacedA = pacedB = keyframeB; + } + // b) Apply distributing offsets in (keyframe A, Paced A] and + // [Paced B, keyframe B). + DistributeRange(Range(keyframeA.get(), + keyframeB - keyframeA + 1), + Range((keyframeA + 1).get(), + pacedA - keyframeA)); + DistributeRange(Range(keyframeA.get(), + keyframeB - keyframeA + 1), + Range(pacedB.get(), + keyframeB - pacedB)); + // c) Apply paced offsets to each paceable keyframe in (Paced A, Paced B). + // We pass the range [Paced A, Paced B] since PaceRange needs the end + // points of the range in order to calculate the correct offset. + PaceRange(Range(pacedA.get(), pacedB - pacedA + 1), + Range(&cumulativeDistances[pacedA - begin], + pacedB - pacedA + 1)); + // d) Fill in any computed offsets in (Paced A, Paced B) that are still + // not set (e.g. because the keyframe was not paceable, or because the + // cumulative distance between paceable properties was zero). + for (RangedPtr frame = pacedA + 1; frame < pacedB; ++frame) { + if (frame->mComputedOffset != Keyframe::kComputedOffsetNotSet) { + continue; + } + RangedPtr start = frame - 1; + RangedPtr end = frame + 1; + while (end < pacedB && + end->mComputedOffset == Keyframe::kComputedOffsetNotSet) { + ++end; + } + DistributeRange(Range(start.get(), end - start + 1)); + frame = end; + } + } keyframeA = keyframeB; } } +/* static */ void +KeyframeUtils::ApplyDistributeSpacing(nsTArray& aKeyframes) +{ + nsTArray emptyArray; + ApplySpacing(aKeyframes, SpacingMode::distribute, eCSSProperty_UNKNOWN, + emptyArray); +} + /* static */ nsTArray KeyframeUtils::GetComputedKeyframeValues(const nsTArray& aKeyframes, dom::Element* aElement, @@ -1358,22 +1447,203 @@ RequiresAdditiveAnimation(const nsTArray& aKeyframes, } /** - * Evenly distribute the computed offsets in (A, B). We should pass the - * range keyframes in [A, B] and use A, B to calculate computed offsets in - * (A, B). + * Evenly distribute the computed offsets in (A, B). + * We pass the range keyframes in [A, B] and use A, B to calculate distributing + * computed offsets in (A, B). The second range, aRangeToAdjust, is passed, so + * we can know which keyframe we want to apply to. aRangeToAdjust should be in + * the range of aSpacingRange. * - * @param aKeyframes The sequence of keyframes between whose endpoints we should - * apply distribute spacing. + * @param aSpacingRange The sequence of keyframes between whose endpoints we + * should apply distribute spacing. + * @param aRangeToAdjust The range of keyframes we want to apply to. */ static void -DistributeRange(const Range& aKeyframes) +DistributeRange(const Range& aSpacingRange, + const Range& aRangeToAdjust) { - const size_t n = aKeyframes.length() - 1; - const double startOffset = aKeyframes[0].mComputedOffset; - const double diffOffset = aKeyframes[n].mComputedOffset - startOffset; - for (size_t i = 1; i < n; ++i) { - aKeyframes[i].mComputedOffset = startOffset + double(i) / n * diffOffset; + MOZ_ASSERT(aRangeToAdjust.start() >= aSpacingRange.start() && + aRangeToAdjust.end() <= aSpacingRange.end(), + "Out of range"); + const size_t n = aSpacingRange.length() - 1; + const double startOffset = aSpacingRange[0].mComputedOffset; + const double diffOffset = aSpacingRange[n].mComputedOffset - startOffset; + for (auto iter = aRangeToAdjust.start(); + iter != aRangeToAdjust.end(); + ++iter) { + size_t index = iter - aSpacingRange.start(); + iter->mComputedOffset = startOffset + double(index) / n * diffOffset; } } +/** + * Overload of DistributeRange to apply distribute spacing to all keyframes in + * between the endpoints of the given range. + * + * @param aSpacingRange The sequence of keyframes between whose endpoints we + * should apply distribute spacing. + */ +static void +DistributeRange(const Range& aSpacingRange) +{ + // We don't need to apply distribute spacing to keyframe A and keyframe B. + DistributeRange(aSpacingRange, + Range((aSpacingRange.start() + 1).get(), + aSpacingRange.end() - aSpacingRange.start() + - 2)); +} + +/** + * Apply paced spacing to all paceable keyframes in between the endpoints of the + * given range. + * + * @param aKeyframes The range of keyframes between whose endpoints we should + * apply paced spacing. Both endpoints should be paceable, i.e. the + * corresponding elements in |aCumulativeDist| should not be kNotPaceable. + * Within this function, we refer to the start and end points of this range + * as Paced A and Paced B respectively in keeping with the notation used in + * the spec. + * @param aCumulativeDistances The sequence of cumulative distances of the paced + * property as returned by GetCumulativeDistances(). This acts as a + * parallel range to |aKeyframes|. + */ +static void +PaceRange(const Range& aKeyframes, + const Range& aCumulativeDistances) +{ + MOZ_ASSERT(aKeyframes.length() == aCumulativeDistances.length(), + "Range length mismatch"); + + const size_t len = aKeyframes.length(); + // If there is nothing between the end points, there is nothing to space. + if (len < 3) { + return; + } + + const double distA = *(aCumulativeDistances.start()); + const double distB = *(aCumulativeDistances.end() - 1); + MOZ_ASSERT(distA != kNotPaceable && distB != kNotPaceable, + "Both Paced A and Paced B should be paceable"); + + // If the total distance is zero, we should fall back to distribute spacing. + // The caller will fill-in any keyframes without a computed offset using + // distribute spacing so we can just return here. + if (distA == distB) { + return; + } + + const RangedPtr pacedA = aKeyframes.start(); + const RangedPtr pacedB = aKeyframes.end() - 1; + MOZ_ASSERT(pacedA->mComputedOffset != Keyframe::kComputedOffsetNotSet && + pacedB->mComputedOffset != Keyframe::kComputedOffsetNotSet, + "Both Paced A and Paced B should have valid computed offsets"); + + // Apply computed offset. + const double offsetA = pacedA->mComputedOffset; + const double diffOffset = pacedB->mComputedOffset - offsetA; + const double initialDist = distA; + const double totalDist = distB - initialDist; + for (auto iter = pacedA + 1; iter != pacedB; ++iter) { + size_t k = iter - aKeyframes.start(); + if (aCumulativeDistances[k] == kNotPaceable) { + continue; + } + + double dist = aCumulativeDistances[k] - initialDist; + iter->mComputedOffset = offsetA + diffOffset * dist / totalDist; + } +} + +/** + * Get cumulative distances for the paced property. + * + * @param aValues The computed values returned by GetComputedKeyframeValues. + * @param aPacedProperty The paced property. + * @return The cumulative distances for the paced property. The length will be + * the same as aValues. + */ +static nsTArray +GetCumulativeDistances(const nsTArray& aValues, + nsCSSProperty aPacedProperty) +{ + // a) If aPacedProperty is a shorthand property, get its components. + // Otherwise, just add the longhand property into the set. + size_t pacedPropertyCount = 0; + nsCSSPropertySet pacedPropertySet; + bool isShorthand = nsCSSProps::IsShorthand(aPacedProperty); + if (isShorthand) { + CSSPROPS_FOR_SHORTHAND_SUBPROPERTIES(p, aPacedProperty, + CSSEnabledState::eForAllContent) { + pacedPropertySet.AddProperty(*p); + ++pacedPropertyCount; + } + } else { + pacedPropertySet.AddProperty(aPacedProperty); + pacedPropertyCount = 1; + } + + // b) Search each component (shorthand) or the longhand property, and + // calculate the cumulative distances of paceable keyframe pairs. + const size_t len = aValues.Length(); + nsTArray cumulativeDistances(len); + // cumulativeDistances is a parallel array to |aValues|, so set its length to + // the length of |aValues|. + cumulativeDistances.SetLength(len); + ComputedKeyframeValues prevPacedValues; + size_t preIdx = 0; + for (size_t i = 0; i < len; ++i) { + // Find computed values of the paced property. + ComputedKeyframeValues pacedValues; + for (const PropertyStyleAnimationValuePair& pair : aValues[i]) { + if (pacedPropertySet.HasProperty(pair.mProperty)) { + pacedValues.AppendElement(pair); + } + } + + // Check we have values for all the paceable longhand components. + if (pacedValues.Length() != pacedPropertyCount) { + // This keyframe is not paceable, assign kNotPaceable and skip it. + cumulativeDistances[i] = kNotPaceable; + continue; + } + + if (prevPacedValues.IsEmpty()) { + // This is the first paceable keyframe so its cumulative distance is 0.0. + cumulativeDistances[i] = 0.0; + } else { + double dist = 0.0; + if (isShorthand) { + // Apply the distance by the square root of the sum of squares of + // longhand component distances. + for (size_t propIdx = 0; propIdx < pacedPropertyCount; ++propIdx) { + nsCSSProperty prop = prevPacedValues[propIdx].mProperty; + MOZ_ASSERT(pacedValues[propIdx].mProperty == prop, + "Property mismatch"); + + double componentDistance = 0.0; + if (StyleAnimationValue::ComputeDistance( + prop, + prevPacedValues[propIdx].mValue, + pacedValues[propIdx].mValue, + componentDistance)) { + dist += componentDistance * componentDistance; + } + } + dist = sqrt(dist); + } else { + // If the property is longhand, we just use the 1st value. + // If ComputeDistance() fails, |dist| will remain zero so there will be + // no distance between the previous paced value and this value. + StyleAnimationValue::ComputeDistance(aPacedProperty, + prevPacedValues[0].mValue, + pacedValues[0].mValue, + dist); + } + cumulativeDistances[i] = cumulativeDistances[preIdx] + dist; + } + prevPacedValues.SwapElements(pacedValues); + preIdx = i; + } + return cumulativeDistances; +} + } // namespace mozilla diff --git a/dom/animation/KeyframeUtils.h b/dom/animation/KeyframeUtils.h index ef025b819c26..7f2580527d38 100644 --- a/dom/animation/KeyframeUtils.h +++ b/dom/animation/KeyframeUtils.h @@ -86,9 +86,25 @@ public: * * @param aKeyframes The set of keyframes to adjust. * @param aSpacingMode The spacing mode to apply. + * @param aProperty The paced property. Only used when |aSpacingMode| is + * SpacingMode::paced. In all other cases it is ignored and hence may be + * any value, e.g. eCSSProperty_UNKNOWN. + * @param aComputedValues The set of computed keyframe values as returned by + * GetComputedKeyframeValues. Only used when |aSpacingMode| is + * SpacingMode::paced. In all other cases this parameter is unused and may + * be any value including an empty array. */ static void ApplySpacing(nsTArray& aKeyframes, - SpacingMode aSpacingMode); + SpacingMode aSpacingMode, + nsCSSProperty aProperty, + nsTArray& aComputedValues); + + /** + * Wrapper for ApplySpacing to simplify using distribute spacing. + * + * @param aKeyframes The set of keyframes to adjust. + */ + static void ApplyDistributeSpacing(nsTArray& aKeyframes); /** * Converts an array of Keyframe objects into an array of AnimationProperty