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