diff options
Diffstat (limited to '')
-rw-r--r-- | dom/smil/SMILAnimationFunction.cpp | 995 |
1 files changed, 995 insertions, 0 deletions
diff --git a/dom/smil/SMILAnimationFunction.cpp b/dom/smil/SMILAnimationFunction.cpp new file mode 100644 index 0000000000..3fe16f7276 --- /dev/null +++ b/dom/smil/SMILAnimationFunction.cpp @@ -0,0 +1,995 @@ +/* -*- 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 "SMILAnimationFunction.h" + +#include <math.h> + +#include <algorithm> +#include <utility> + +#include "mozilla/DebugOnly.h" +#include "mozilla/SMILAttr.h" +#include "mozilla/SMILCSSValueType.h" +#include "mozilla/SMILNullType.h" +#include "mozilla/SMILParserUtils.h" +#include "mozilla/SMILTimedElement.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsAttrValueInlines.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsReadableUtils.h" +#include "nsString.h" + +using namespace mozilla::dom; + +namespace mozilla { + +//---------------------------------------------------------------------- +// Static members + +nsAttrValue::EnumTable SMILAnimationFunction::sAccumulateTable[] = { + {"none", false}, {"sum", true}, {nullptr, 0}}; + +nsAttrValue::EnumTable SMILAnimationFunction::sAdditiveTable[] = { + {"replace", false}, {"sum", true}, {nullptr, 0}}; + +nsAttrValue::EnumTable SMILAnimationFunction::sCalcModeTable[] = { + {"linear", CALC_LINEAR}, + {"discrete", CALC_DISCRETE}, + {"paced", CALC_PACED}, + {"spline", CALC_SPLINE}, + {nullptr, 0}}; + +// Any negative number should be fine as a sentinel here, +// because valid distances are non-negative. +#define COMPUTE_DISTANCE_ERROR (-1) + +//---------------------------------------------------------------------- +// Constructors etc. + +SMILAnimationFunction::SMILAnimationFunction() + : mSampleTime(-1), + mRepeatIteration(0), + mBeginTime(INT64_MIN), + mAnimationElement(nullptr), + mErrorFlags(0), + mIsActive(false), + mIsFrozen(false), + mLastValue(false), + mHasChanged(true), + mValueNeedsReparsingEverySample(false), + mPrevSampleWasSingleValueAnimation(false), + mWasSkippedInPrevSample(false) {} + +void SMILAnimationFunction::SetAnimationElement( + SVGAnimationElement* aAnimationElement) { + mAnimationElement = aAnimationElement; +} + +bool SMILAnimationFunction::SetAttr(nsAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, + nsresult* aParseResult) { + // Some elements such as set and discard don't support all possible attributes + if (IsDisallowedAttribute(aAttribute)) { + aResult.SetTo(aValue); + if (aParseResult) { + *aParseResult = NS_OK; + } + return true; + } + + bool foundMatch = true; + nsresult parseResult = NS_OK; + + // The attributes 'by', 'from', 'to', and 'values' may be parsed differently + // depending on the element & attribute we're animating. So instead of + // parsing them now we re-parse them at every sample. + if (aAttribute == nsGkAtoms::by || aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || aAttribute == nsGkAtoms::values) { + // We parse to, from, by, values at sample time. + // XXX Need to flag which attribute has changed and then when we parse it at + // sample time, report any errors and reset the flag + mHasChanged = true; + aResult.SetTo(aValue); + } else if (aAttribute == nsGkAtoms::accumulate) { + parseResult = SetAccumulate(aValue, aResult); + } else if (aAttribute == nsGkAtoms::additive) { + parseResult = SetAdditive(aValue, aResult); + } else if (aAttribute == nsGkAtoms::calcMode) { + parseResult = SetCalcMode(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keyTimes) { + parseResult = SetKeyTimes(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keySplines) { + parseResult = SetKeySplines(aValue, aResult); + } else { + foundMatch = false; + } + + if (foundMatch && aParseResult) { + *aParseResult = parseResult; + } + + return foundMatch; +} + +bool SMILAnimationFunction::UnsetAttr(nsAtom* aAttribute) { + if (IsDisallowedAttribute(aAttribute)) { + return true; + } + + bool foundMatch = true; + + if (aAttribute == nsGkAtoms::by || aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || aAttribute == nsGkAtoms::values) { + mHasChanged = true; + } else if (aAttribute == nsGkAtoms::accumulate) { + UnsetAccumulate(); + } else if (aAttribute == nsGkAtoms::additive) { + UnsetAdditive(); + } else if (aAttribute == nsGkAtoms::calcMode) { + UnsetCalcMode(); + } else if (aAttribute == nsGkAtoms::keyTimes) { + UnsetKeyTimes(); + } else if (aAttribute == nsGkAtoms::keySplines) { + UnsetKeySplines(); + } else { + foundMatch = false; + } + + return foundMatch; +} + +void SMILAnimationFunction::SampleAt(SMILTime aSampleTime, + const SMILTimeValue& aSimpleDuration, + uint32_t aRepeatIteration) { + // * Update mHasChanged ("Might this sample be different from prev one?") + // Were we previously sampling a fill="freeze" final val? (We're not anymore.) + mHasChanged |= mLastValue; + + // Are we sampling at a new point in simple duration? And does that matter? + mHasChanged |= + (mSampleTime != aSampleTime || mSimpleDuration != aSimpleDuration) && + !IsValueFixedForSimpleDuration(); + + // Are we on a new repeat and accumulating across repeats? + if (!mErrorFlags) { // (can't call GetAccumulate() if we've had parse errors) + mHasChanged |= (mRepeatIteration != aRepeatIteration) && GetAccumulate(); + } + + mSampleTime = aSampleTime; + mSimpleDuration = aSimpleDuration; + mRepeatIteration = aRepeatIteration; + mLastValue = false; +} + +void SMILAnimationFunction::SampleLastValue(uint32_t aRepeatIteration) { + if (mHasChanged || !mLastValue || mRepeatIteration != aRepeatIteration) { + mHasChanged = true; + } + + mRepeatIteration = aRepeatIteration; + mLastValue = true; +} + +void SMILAnimationFunction::Activate(SMILTime aBeginTime) { + mBeginTime = aBeginTime; + mIsActive = true; + mIsFrozen = false; + mHasChanged = true; +} + +void SMILAnimationFunction::Inactivate(bool aIsFrozen) { + mIsActive = false; + mIsFrozen = aIsFrozen; + mHasChanged = true; +} + +void SMILAnimationFunction::ComposeResult(const SMILAttr& aSMILAttr, + SMILValue& aResult) { + mHasChanged = false; + mPrevSampleWasSingleValueAnimation = false; + mWasSkippedInPrevSample = false; + + // Skip animations that are inactive or in error + if (!IsActiveOrFrozen() || mErrorFlags != 0) return; + + // Get the animation values + SMILValueArray values; + nsresult rv = GetValues(aSMILAttr, values); + if (NS_FAILED(rv)) return; + + // Check that we have the right number of keySplines and keyTimes + CheckValueListDependentAttrs(values.Length()); + if (mErrorFlags != 0) return; + + // If this interval is active, we must have a non-negative mSampleTime + MOZ_ASSERT(mSampleTime >= 0 || !mIsActive, + "Negative sample time for active animation"); + MOZ_ASSERT(mSimpleDuration.IsResolved() || mLastValue, + "Unresolved simple duration for active or frozen animation"); + + // If we want to add but don't have a base value then just fail outright. + // This can happen when we skipped getting the base value because there's an + // animation function in the sandwich that should replace it but that function + // failed unexpectedly. + bool isAdditive = IsAdditive(); + if (isAdditive && aResult.IsNull()) return; + + SMILValue result; + + if (values.Length() == 1 && !IsToAnimation()) { + // Single-valued animation + result = values[0]; + mPrevSampleWasSingleValueAnimation = true; + + } else if (mLastValue) { + // Sampling last value + const SMILValue& last = values[values.Length() - 1]; + result = last; + + // See comment in AccumulateResult: to-animation does not accumulate + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + // If the target attribute type doesn't support addition Add will + // fail leaving result = last + result.Add(last, mRepeatIteration); + } + + } else { + // Interpolation + if (NS_FAILED(InterpolateResult(values, result, aResult))) return; + + if (NS_FAILED(AccumulateResult(values, result))) return; + } + + // If additive animation isn't required or isn't supported, set the value. + if (!isAdditive || NS_FAILED(aResult.SandwichAdd(result))) { + aResult = std::move(result); + } +} + +int8_t SMILAnimationFunction::CompareTo( + const SMILAnimationFunction* aOther) const { + NS_ENSURE_TRUE(aOther, 0); + + NS_ASSERTION(aOther != this, "Trying to compare to self"); + + // Inactive animations sort first + if (!IsActiveOrFrozen() && aOther->IsActiveOrFrozen()) return -1; + + if (IsActiveOrFrozen() && !aOther->IsActiveOrFrozen()) return 1; + + // Sort based on begin time + if (mBeginTime != aOther->GetBeginTime()) + return mBeginTime > aOther->GetBeginTime() ? 1 : -1; + + // Next sort based on syncbase dependencies: the dependent element sorts after + // its syncbase + const SMILTimedElement& thisTimedElement = mAnimationElement->TimedElement(); + const SMILTimedElement& otherTimedElement = + aOther->mAnimationElement->TimedElement(); + if (thisTimedElement.IsTimeDependent(otherTimedElement)) return 1; + if (otherTimedElement.IsTimeDependent(thisTimedElement)) return -1; + + // Animations that appear later in the document sort after those earlier in + // the document + MOZ_ASSERT(mAnimationElement != aOther->mAnimationElement, + "Two animations cannot have the same animation content element!"); + + return (nsContentUtils::PositionIsBefore(mAnimationElement, + aOther->mAnimationElement)) + ? -1 + : 1; +} + +bool SMILAnimationFunction::WillReplace() const { + /* + * In IsAdditive() we don't consider to-animation to be additive as it is + * a special case that is dealt with differently in the compositing method. + * Here, however, we return FALSE for to-animation (i.e. it will NOT replace + * the underlying value) as it builds on the underlying value. + */ + return !mErrorFlags && !(IsAdditive() || IsToAnimation()); +} + +bool SMILAnimationFunction::HasChanged() const { + return mHasChanged || mValueNeedsReparsingEverySample; +} + +bool SMILAnimationFunction::UpdateCachedTarget( + const SMILTargetIdentifier& aNewTarget) { + if (!mLastTarget.Equals(aNewTarget)) { + mLastTarget = aNewTarget; + return true; + } + return false; +} + +//---------------------------------------------------------------------- +// Implementation helpers + +nsresult SMILAnimationFunction::InterpolateResult(const SMILValueArray& aValues, + SMILValue& aResult, + SMILValue& aBaseValue) { + // Sanity check animation values + if ((!IsToAnimation() && aValues.Length() < 2) || + (IsToAnimation() && aValues.Length() != 1)) { + NS_ERROR("Unexpected number of values"); + return NS_ERROR_FAILURE; + } + + if (IsToAnimation() && aBaseValue.IsNull()) { + return NS_ERROR_FAILURE; + } + + // Get the normalised progress through the simple duration. + // + // If we have an indefinite simple duration, just set the progress to be + // 0 which will give us the expected behaviour of the animation being fixed at + // its starting point. + double simpleProgress = 0.0; + + if (mSimpleDuration.IsDefinite()) { + SMILTime dur = mSimpleDuration.GetMillis(); + + MOZ_ASSERT(dur >= 0, "Simple duration should not be negative"); + MOZ_ASSERT(mSampleTime >= 0, "Sample time should not be negative"); + + if (mSampleTime >= dur || mSampleTime < 0) { + NS_ERROR("Animation sampled outside interval"); + return NS_ERROR_FAILURE; + } + + if (dur > 0) { + simpleProgress = (double)mSampleTime / dur; + } // else leave simpleProgress at 0.0 (e.g. if mSampleTime == dur == 0) + } + + nsresult rv = NS_OK; + SMILCalcMode calcMode = GetCalcMode(); + + // Force discrete calcMode for visibility since StyleAnimationValue will + // try to interpolate it using the special clamping behavior defined for + // CSS. + if (SMILCSSValueType::PropertyFromValue(aValues[0]) == + eCSSProperty_visibility) { + calcMode = CALC_DISCRETE; + } + + if (calcMode != CALC_DISCRETE) { + // Get the normalised progress between adjacent values + const SMILValue* from = nullptr; + const SMILValue* to = nullptr; + // Init to -1 to make sure that if we ever forget to set this, the + // MOZ_ASSERT that tests that intervalProgress is in range will fail. + double intervalProgress = -1.f; + if (IsToAnimation()) { + from = &aBaseValue; + to = &aValues[0]; + if (calcMode == CALC_PACED) { + // Note: key[Times/Splines/Points] are ignored for calcMode="paced" + intervalProgress = simpleProgress; + } else { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + intervalProgress = ScaleIntervalProgress(scaledSimpleProgress, 0); + } + } else if (calcMode == CALC_PACED) { + rv = ComputePacedPosition(aValues, simpleProgress, intervalProgress, from, + to); + // Note: If the above call fails, we'll skip the "from->Interpolate" + // call below, and we'll drop into the CALC_DISCRETE section + // instead. (as the spec says we should, because our failure was + // presumably due to the values being non-additive) + } else { // calcMode == CALC_LINEAR or calcMode == CALC_SPLINE + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + uint32_t index = + (uint32_t)floor(scaledSimpleProgress * (aValues.Length() - 1)); + from = &aValues[index]; + to = &aValues[index + 1]; + intervalProgress = scaledSimpleProgress * (aValues.Length() - 1) - index; + intervalProgress = ScaleIntervalProgress(intervalProgress, index); + } + + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(from, "NULL from-value during interpolation"); + MOZ_ASSERT(to, "NULL to-value during interpolation"); + MOZ_ASSERT(0.0f <= intervalProgress && intervalProgress < 1.0f, + "Interval progress should be in the range [0, 1)"); + rv = from->Interpolate(*to, intervalProgress, aResult); + } + } + + // Discrete-CalcMode case + // Note: If interpolation failed (isn't supported for this type), the SVG + // spec says to force discrete mode. + if (calcMode == CALC_DISCRETE || NS_FAILED(rv)) { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, CALC_DISCRETE); + + // Floating-point errors can mean that, for example, a sample time of 29s in + // a 100s duration animation gives us a simple progress of 0.28999999999 + // instead of the 0.29 we'd expect. Normally this isn't a noticeable + // problem, but when we have sudden jumps in animation values (such as is + // the case here with discrete animation) we can get unexpected results. + // + // To counteract this, before we perform a floor() on the animation + // progress, we add a tiny fudge factor to push us into the correct interval + // in cases where floating-point errors might cause us to fall short. + static const double kFloatingPointFudgeFactor = 1.0e-16; + if (scaledSimpleProgress + kFloatingPointFudgeFactor <= 1.0) { + scaledSimpleProgress += kFloatingPointFudgeFactor; + } + + if (IsToAnimation()) { + // We don't follow SMIL 3, 12.6.4, where discrete to animations + // are the same as <set> animations. Instead, we treat it as a + // discrete animation with two values (the underlying value and + // the to="" value), and honor keyTimes="" as well. + uint32_t index = (uint32_t)floor(scaledSimpleProgress * 2); + aResult = index == 0 ? aBaseValue : aValues[0]; + } else { + uint32_t index = (uint32_t)floor(scaledSimpleProgress * aValues.Length()); + aResult = aValues[index]; + + // For animation of CSS properties, normally when interpolating we perform + // a zero-value fixup which means that empty values (values with type + // SMILCSSValueType but a null pointer value) are converted into + // a suitable zero value based on whatever they're being interpolated + // with. For discrete animation, however, since we don't interpolate, + // that never happens. In some rare cases, such as discrete non-additive + // by-animation, we can arrive here with |aResult| being such an empty + // value so we need to manually perform the fixup. + // + // We could define a generic method for this on SMILValue but its faster + // and simpler to just special case SMILCSSValueType. + if (aResult.mType == &SMILCSSValueType::sSingleton) { + // We have currently only ever encountered this case for the first + // value of a by-animation (which has two values) and since we have no + // way of testing other cases we just skip them (but assert if we + // ever do encounter them so that we can add code to handle them). + if (index + 1 >= aValues.Length()) { + MOZ_ASSERT(aResult.mU.mPtr, "The last value should not be empty"); + } else { + // Base the type of the zero value on the next element in the series. + SMILCSSValueType::FinalizeValue(aResult, aValues[index + 1]); + } + } + } + rv = NS_OK; + } + return rv; +} + +nsresult SMILAnimationFunction::AccumulateResult(const SMILValueArray& aValues, + SMILValue& aResult) { + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + const SMILValue& lastValue = aValues[aValues.Length() - 1]; + + // If the target attribute type doesn't support addition, Add will + // fail and we leave aResult untouched. + aResult.Add(lastValue, mRepeatIteration); + } + + return NS_OK; +} + +/* + * Given the simple progress for a paced animation, this method: + * - determines which two elements of the values array we're in between + * (returned as aFrom and aTo) + * - determines where we are between them + * (returned as aIntervalProgress) + * + * Returns NS_OK, or NS_ERROR_FAILURE if our values don't support distance + * computation. + */ +nsresult SMILAnimationFunction::ComputePacedPosition( + const SMILValueArray& aValues, double aSimpleProgress, + double& aIntervalProgress, const SMILValue*& aFrom, const SMILValue*& aTo) { + NS_ASSERTION(0.0f <= aSimpleProgress && aSimpleProgress < 1.0f, + "aSimpleProgress is out of bounds"); + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + MOZ_ASSERT(aValues.Length() >= 2, "Unexpected number of values"); + + // Trivial case: If we have just 2 values, then there's only one interval + // for us to traverse, and our progress across that interval is the exact + // same as our overall progress. + if (aValues.Length() == 2) { + aIntervalProgress = aSimpleProgress; + aFrom = &aValues[0]; + aTo = &aValues[1]; + return NS_OK; + } + + double totalDistance = ComputePacedTotalDistance(aValues); + if (totalDistance == COMPUTE_DISTANCE_ERROR) return NS_ERROR_FAILURE; + + // If we have 0 total distance, then it's unclear where our "paced" position + // should be. We can just fail, which drops us into discrete animation mode. + // (That's fine, since our values are apparently indistinguishable anyway.) + if (totalDistance == 0.0) { + return NS_ERROR_FAILURE; + } + + // total distance we should have moved at this point in time. + // (called 'remainingDist' due to how it's used in loop below) + double remainingDist = aSimpleProgress * totalDistance; + + // Must be satisfied, because totalDistance is a sum of (non-negative) + // distances, and aSimpleProgress is non-negative + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + // Find where remainingDist puts us in the list of values + // Note: We could optimize this next loop by caching the + // interval-distances in an array, but maybe that's excessive. + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + // Note: The following assertion is valid because remainingDist should + // start out non-negative, and this loop never shaves off more than its + // current value. + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + double curIntervalDist; + + DebugOnly<nsresult> rv = + aValues[i].ComputeDistance(aValues[i + 1], curIntervalDist); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "If we got through ComputePacedTotalDistance, we should " + "be able to recompute each sub-distance without errors"); + + NS_ASSERTION(curIntervalDist >= 0, "distance values must be non-negative"); + // Clamp distance value at 0, just in case ComputeDistance is evil. + curIntervalDist = std::max(curIntervalDist, 0.0); + + if (remainingDist >= curIntervalDist) { + remainingDist -= curIntervalDist; + } else { + // NOTE: If we get here, then curIntervalDist necessarily is not 0. Why? + // Because this clause is only hit when remainingDist < curIntervalDist, + // and if curIntervalDist were 0, that would mean remainingDist would + // have to be < 0. But that can't happen, because remainingDist (as + // a distance) is non-negative by definition. + NS_ASSERTION(curIntervalDist != 0, + "We should never get here with this set to 0..."); + + // We found the right spot -- an interpolated position between + // values i and i+1. + aFrom = &aValues[i]; + aTo = &aValues[i + 1]; + aIntervalProgress = remainingDist / curIntervalDist; + return NS_OK; + } + } + + MOZ_ASSERT_UNREACHABLE( + "shouldn't complete loop & get here -- if we do, " + "then aSimpleProgress was probably out of bounds"); + return NS_ERROR_FAILURE; +} + +/* + * Computes the total distance to be travelled by a paced animation. + * + * Returns the total distance, or returns COMPUTE_DISTANCE_ERROR if + * our values don't support distance computation. + */ +double SMILAnimationFunction::ComputePacedTotalDistance( + const SMILValueArray& aValues) const { + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + + double totalDistance = 0.0; + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + double tmpDist; + nsresult rv = aValues[i].ComputeDistance(aValues[i + 1], tmpDist); + if (NS_FAILED(rv)) { + return COMPUTE_DISTANCE_ERROR; + } + + // Clamp distance value to 0, just in case we have an evil ComputeDistance + // implementation somewhere + MOZ_ASSERT(tmpDist >= 0.0f, "distance values must be non-negative"); + tmpDist = std::max(tmpDist, 0.0); + + totalDistance += tmpDist; + } + + return totalDistance; +} + +double SMILAnimationFunction::ScaleSimpleProgress(double aProgress, + SMILCalcMode aCalcMode) { + if (!HasAttr(nsGkAtoms::keyTimes)) return aProgress; + + uint32_t numTimes = mKeyTimes.Length(); + + if (numTimes < 2) return aProgress; + + uint32_t i = 0; + for (; i < numTimes - 2 && aProgress >= mKeyTimes[i + 1]; ++i) { + } + + if (aCalcMode == CALC_DISCRETE) { + // discrete calcMode behaviour differs in that each keyTime defines the time + // from when the corresponding value is set, and therefore the last value + // needn't be 1. So check if we're in the last 'interval', that is, the + // space between the final value and 1.0. + if (aProgress >= mKeyTimes[i + 1]) { + MOZ_ASSERT(i == numTimes - 2, + "aProgress is not in range of the current interval, yet the " + "current interval is not the last bounded interval either."); + ++i; + } + return (double)i / numTimes; + } + + double& intervalStart = mKeyTimes[i]; + double& intervalEnd = mKeyTimes[i + 1]; + + double intervalLength = intervalEnd - intervalStart; + if (intervalLength <= 0.0) return intervalStart; + + return (i + (aProgress - intervalStart) / intervalLength) / + double(numTimes - 1); +} + +double SMILAnimationFunction::ScaleIntervalProgress(double aProgress, + uint32_t aIntervalIndex) { + if (GetCalcMode() != CALC_SPLINE) return aProgress; + + if (!HasAttr(nsGkAtoms::keySplines)) return aProgress; + + MOZ_ASSERT(aIntervalIndex < mKeySplines.Length(), "Invalid interval index"); + + SMILKeySpline const& spline = mKeySplines[aIntervalIndex]; + return spline.GetSplineValue(aProgress); +} + +bool SMILAnimationFunction::HasAttr(nsAtom* aAttName) const { + if (IsDisallowedAttribute(aAttName)) { + return false; + } + return mAnimationElement->HasAttr(aAttName); +} + +const nsAttrValue* SMILAnimationFunction::GetAttr(nsAtom* aAttName) const { + if (IsDisallowedAttribute(aAttName)) { + return nullptr; + } + return mAnimationElement->GetParsedAttr(aAttName); +} + +bool SMILAnimationFunction::GetAttr(nsAtom* aAttName, + nsAString& aResult) const { + if (IsDisallowedAttribute(aAttName)) { + return false; + } + return mAnimationElement->GetAttr(aAttName, aResult); +} + +/* + * A utility function to make querying an attribute that corresponds to an + * SMILValue a little neater. + * + * @param aAttName The attribute name (in the global namespace). + * @param aSMILAttr The SMIL attribute to perform the parsing. + * @param[out] aResult The resulting SMILValue. + * @param[out] aPreventCachingOfSandwich + * If |aResult| contains dependencies on its context that + * should prevent the result of the animation sandwich from + * being cached and reused in future samples (as reported + * by SMILAttr::ValueFromString), then this outparam + * will be set to true. Otherwise it is left unmodified. + * + * Returns false if a parse error occurred, otherwise returns true. + */ +bool SMILAnimationFunction::ParseAttr(nsAtom* aAttName, + const SMILAttr& aSMILAttr, + SMILValue& aResult, + bool& aPreventCachingOfSandwich) const { + nsAutoString attValue; + if (GetAttr(aAttName, attValue)) { + nsresult rv = aSMILAttr.ValueFromString(attValue, mAnimationElement, + aResult, aPreventCachingOfSandwich); + if (NS_FAILED(rv)) return false; + } + return true; +} + +/* + * SMILANIM specifies the following rules for animation function values: + * + * (1) if values is set, it overrides everything + * (2) for from/to/by animation at least to or by must be specified, from on its + * own (or nothing) is an error--which we will ignore + * (3) if both by and to are specified only to will be used, by will be ignored + * (4) if by is specified without from (by animation), forces additive behaviour + * (5) if to is specified without from (to animation), special care needs to be + * taken when compositing animation as such animations are composited last. + * + * This helper method applies these rules to fill in the values list and to set + * some internal state. + */ +nsresult SMILAnimationFunction::GetValues(const SMILAttr& aSMILAttr, + SMILValueArray& aResult) { + if (!mAnimationElement) return NS_ERROR_FAILURE; + + mValueNeedsReparsingEverySample = false; + SMILValueArray result; + + // If "values" is set, use it + if (HasAttr(nsGkAtoms::values)) { + nsAutoString attValue; + GetAttr(nsGkAtoms::values, attValue); + bool preventCachingOfSandwich = false; + if (!SMILParserUtils::ParseValues(attValue, mAnimationElement, aSMILAttr, + result, preventCachingOfSandwich)) { + return NS_ERROR_FAILURE; + } + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + // Else try to/from/by + } else { + bool preventCachingOfSandwich = false; + bool parseOk = true; + SMILValue to, from, by; + parseOk &= + ParseAttr(nsGkAtoms::to, aSMILAttr, to, preventCachingOfSandwich); + parseOk &= + ParseAttr(nsGkAtoms::from, aSMILAttr, from, preventCachingOfSandwich); + parseOk &= + ParseAttr(nsGkAtoms::by, aSMILAttr, by, preventCachingOfSandwich); + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + + if (!parseOk || !result.SetCapacity(2, fallible)) { + return NS_ERROR_FAILURE; + } + + // AppendElement() below must succeed, because SetCapacity() succeeded. + if (!to.IsNull()) { + if (!from.IsNull()) { + MOZ_ALWAYS_TRUE(result.AppendElement(from, fallible)); + MOZ_ALWAYS_TRUE(result.AppendElement(to, fallible)); + } else { + MOZ_ALWAYS_TRUE(result.AppendElement(to, fallible)); + } + } else if (!by.IsNull()) { + SMILValue effectiveFrom(by.mType); + if (!from.IsNull()) effectiveFrom = from; + // Set values to 'from; from + by' + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveFrom, fallible)); + SMILValue effectiveTo(effectiveFrom); + if (!effectiveTo.IsNull() && NS_SUCCEEDED(effectiveTo.Add(by))) { + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveTo, fallible)); + } else { + // Using by-animation with non-additive type or bad base-value + return NS_ERROR_FAILURE; + } + } else { + // No values, no to, no by -- call it a day + return NS_ERROR_FAILURE; + } + } + + aResult = std::move(result); + + return NS_OK; +} + +void SMILAnimationFunction::CheckValueListDependentAttrs(uint32_t aNumValues) { + CheckKeyTimes(aNumValues); + CheckKeySplines(aNumValues); +} + +/** + * Performs checks for the keyTimes attribute required by the SMIL spec but + * which depend on other attributes and therefore needs to be updated as + * dependent attributes are set. + */ +void SMILAnimationFunction::CheckKeyTimes(uint32_t aNumValues) { + if (!HasAttr(nsGkAtoms::keyTimes)) return; + + SMILCalcMode calcMode = GetCalcMode(); + + // attribute is ignored for calcMode = paced + if (calcMode == CALC_PACED) { + SetKeyTimesErrorFlag(false); + return; + } + + uint32_t numKeyTimes = mKeyTimes.Length(); + if (numKeyTimes < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeyTimesErrorFlag(true); + return; + } + + // no. keyTimes == no. values + // For to-animation the number of values is considered to be 2. + bool matchingNumOfValues = numKeyTimes == (IsToAnimation() ? 2 : aNumValues); + if (!matchingNumOfValues) { + SetKeyTimesErrorFlag(true); + return; + } + + // first value must be 0 + if (mKeyTimes[0] != 0.0) { + SetKeyTimesErrorFlag(true); + return; + } + + // last value must be 1 for linear or spline calcModes + if (calcMode != CALC_DISCRETE && numKeyTimes > 1 && + mKeyTimes[numKeyTimes - 1] != 1.0) { + SetKeyTimesErrorFlag(true); + return; + } + + SetKeyTimesErrorFlag(false); +} + +void SMILAnimationFunction::CheckKeySplines(uint32_t aNumValues) { + // attribute is ignored if calc mode is not spline + if (GetCalcMode() != CALC_SPLINE) { + SetKeySplinesErrorFlag(false); + return; + } + + // calc mode is spline but the attribute is not set + if (!HasAttr(nsGkAtoms::keySplines)) { + SetKeySplinesErrorFlag(false); + return; + } + + if (mKeySplines.Length() < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeySplinesErrorFlag(true); + return; + } + + // ignore splines if there's only one value + if (aNumValues == 1 && !IsToAnimation()) { + SetKeySplinesErrorFlag(false); + return; + } + + // no. keySpline specs == no. values - 1 + uint32_t splineSpecs = mKeySplines.Length(); + if ((splineSpecs != aNumValues - 1 && !IsToAnimation()) || + (IsToAnimation() && splineSpecs != 1)) { + SetKeySplinesErrorFlag(true); + return; + } + + SetKeySplinesErrorFlag(false); +} + +bool SMILAnimationFunction::IsValueFixedForSimpleDuration() const { + return mSimpleDuration.IsIndefinite() || + (!mHasChanged && mPrevSampleWasSingleValueAnimation); +} + +//---------------------------------------------------------------------- +// Property getters + +bool SMILAnimationFunction::GetAccumulate() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::accumulate); + if (!value) return false; + + return value->GetEnumValue(); +} + +bool SMILAnimationFunction::GetAdditive() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::additive); + if (!value) return false; + + return value->GetEnumValue(); +} + +SMILAnimationFunction::SMILCalcMode SMILAnimationFunction::GetCalcMode() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode); + if (!value) return CALC_LINEAR; + + return SMILCalcMode(value->GetEnumValue()); +} + +//---------------------------------------------------------------------- +// Property setters / un-setters: + +nsresult SMILAnimationFunction::SetAccumulate(const nsAString& aAccumulate, + nsAttrValue& aResult) { + mHasChanged = true; + bool parseResult = + aResult.ParseEnumValue(aAccumulate, sAccumulateTable, true); + SetAccumulateErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILAnimationFunction::UnsetAccumulate() { + SetAccumulateErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetAdditive(const nsAString& aAdditive, + nsAttrValue& aResult) { + mHasChanged = true; + bool parseResult = aResult.ParseEnumValue(aAdditive, sAdditiveTable, true); + SetAdditiveErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILAnimationFunction::UnsetAdditive() { + SetAdditiveErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetCalcMode(const nsAString& aCalcMode, + nsAttrValue& aResult) { + mHasChanged = true; + bool parseResult = aResult.ParseEnumValue(aCalcMode, sCalcModeTable, true); + SetCalcModeErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILAnimationFunction::UnsetCalcMode() { + SetCalcModeErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetKeySplines(const nsAString& aKeySplines, + nsAttrValue& aResult) { + mKeySplines.Clear(); + aResult.SetTo(aKeySplines); + + mHasChanged = true; + + if (!SMILParserUtils::ParseKeySplines(aKeySplines, mKeySplines)) { + mKeySplines.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void SMILAnimationFunction::UnsetKeySplines() { + mKeySplines.Clear(); + SetKeySplinesErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetKeyTimes(const nsAString& aKeyTimes, + nsAttrValue& aResult) { + mKeyTimes.Clear(); + aResult.SetTo(aKeyTimes); + + mHasChanged = true; + + if (!SMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyTimes, true, + mKeyTimes)) { + mKeyTimes.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void SMILAnimationFunction::UnsetKeyTimes() { + mKeyTimes.Clear(); + SetKeyTimesErrorFlag(false); + mHasChanged = true; +} + +} // namespace mozilla |