/* -*- 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 #include #include #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 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 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