gecko-dev/content/smil/nsSMILAnimationFunction.cpp

1079 lines
33 KiB
C++
Raw Normal View History

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2012-05-21 11:12:37 +00:00
/* 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 "mozilla/dom/SVGAnimationElement.h"
#include "nsSMILAnimationFunction.h"
#include "nsISMILAttr.h"
#include "nsSMILParserUtils.h"
#include "nsSMILNullType.h"
#include "nsSMILTimedElement.h"
Bug 760331: Coalesce data for inline style across nodes. r=bz This patch enables sharing of an nsAttrValue's MiscContainer between nodes for style rules. MiscContainers of type eCSSStyleRule are now refcounted (with some clever struct packing to ensure that the amount of memory allocated for MiscContainer remains unchanged on 32 and 64 bit). This infrastructure can be used to share most MiscContainer types in the future if we find advantages to sharing other types than just eCSSStyleRuley. A cache mapping strings to MiscContainers has been added to nsHTMLCSSStyleSheet. MiscContainers can be shared between nsAttrValues when one nsAttrValue is SetTo another nsAttrValue or when there is a cache hit in this cache. This patch also adds the ability to tell a style rule that it belongs to an nsHTMLCSSStyleSheet, with appropriate accessor functions to separate that from the existing case of belonging to an nsCSSStyleSheet. The primary use case is to reduce memory use for pages that have lots of inline style attributes with the same value. This can happen easily with large pages that are automatically generated. An (admittedly pathological) testcase in Bug 686975 sees over 250 MB of memory savings with this change. Reusing the same MiscContainer for multiple nodes saves the overhead of maintaining separate copies of the string containing the serialized value of the style attribute and of creating separate style rules for each node. Eliminating duplicate style rules enables further savings in layout through style context sharing. The testcase sees the amount of memory used by style contexts go from over 250 MB to 10 KB. Because the cache is based on the text value of the style attribute, it will not handle attributes that have different text values but are parsed into identical style rules. We also do not attempt to share MiscContainers when the node's base URI differs from the document URI. The effect of these limitations is expected to be low.
2012-09-30 16:40:24 +00:00
#include "nsAttrValueInlines.h"
#include "nsGkAtoms.h"
#include "nsCOMPtr.h"
#include "nsCOMArray.h"
#include "nsIContent.h"
#include "nsAutoPtr.h"
#include "nsContentUtils.h"
#include "nsReadableUtils.h"
#include "nsString.h"
#include <math.h>
#include <algorithm>
using namespace mozilla::dom;
//----------------------------------------------------------------------
// Static members
nsAttrValue::EnumTable nsSMILAnimationFunction::sAccumulateTable[] = {
{"none", false},
{"sum", true},
{nullptr, 0}
};
nsAttrValue::EnumTable nsSMILAnimationFunction::sAdditiveTable[] = {
{"replace", false},
{"sum", true},
{nullptr, 0}
};
nsAttrValue::EnumTable nsSMILAnimationFunction::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.
nsSMILAnimationFunction::nsSMILAnimationFunction()
: 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
nsSMILAnimationFunction::SetAnimationElement(
SVGAnimationElement* aAnimationElement)
{
mAnimationElement = aAnimationElement;
}
bool
nsSMILAnimationFunction::SetAttr(nsIAtom* aAttribute, const nsAString& aValue,
nsAttrValue& aResult, nsresult* aParseResult)
{
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
nsSMILAnimationFunction::UnsetAttr(nsIAtom* aAttribute)
{
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
nsSMILAnimationFunction::SampleAt(nsSMILTime aSampleTime,
const nsSMILTimeValue& 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
nsSMILAnimationFunction::SampleLastValue(uint32_t aRepeatIteration)
{
if (mHasChanged || !mLastValue || mRepeatIteration != aRepeatIteration) {
mHasChanged = true;
}
mRepeatIteration = aRepeatIteration;
mLastValue = true;
}
void
nsSMILAnimationFunction::Activate(nsSMILTime aBeginTime)
{
mBeginTime = aBeginTime;
mIsActive = true;
mIsFrozen = false;
mHasChanged = true;
}
void
nsSMILAnimationFunction::Inactivate(bool aIsFrozen)
{
mIsActive = false;
mIsFrozen = aIsFrozen;
mHasChanged = true;
}
void
nsSMILAnimationFunction::ComposeResult(const nsISMILAttr& aSMILAttr,
nsSMILValue& aResult)
{
mHasChanged = false;
mPrevSampleWasSingleValueAnimation = false;
mWasSkippedInPrevSample = false;
// Skip animations that are inactive or in error
if (!IsActiveOrFrozen() || mErrorFlags != 0)
return;
// Get the animation values
nsSMILValueArray 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
NS_ABORT_IF_FALSE(mSampleTime >= 0 || !mIsActive,
"Negative sample time for active animation");
NS_ABORT_IF_FALSE(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;
nsSMILValue result;
if (values.Length() == 1 && !IsToAnimation()) {
// Single-valued animation
result = values[0];
mPrevSampleWasSingleValueAnimation = true;
} else if (mLastValue) {
// Sampling last value
const nsSMILValue& 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.Swap(result);
// Note: The old value of aResult is now in |result|, and it will get
// cleaned up when |result| goes out of scope, when this function returns.
}
}
int8_t
nsSMILAnimationFunction::CompareTo(const nsSMILAnimationFunction* 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 nsSMILTimedElement& thisTimedElement =
mAnimationElement->TimedElement();
const nsSMILTimedElement& 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
NS_ABORT_IF_FALSE(mAnimationElement != aOther->mAnimationElement,
"Two animations cannot have the same animation content element!");
return (nsContentUtils::PositionIsBefore(mAnimationElement, aOther->mAnimationElement))
? -1 : 1;
}
bool
nsSMILAnimationFunction::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
nsSMILAnimationFunction::HasChanged() const
{
return mHasChanged || mValueNeedsReparsingEverySample;
}
bool
nsSMILAnimationFunction::UpdateCachedTarget(
const nsSMILTargetIdentifier& aNewTarget)
{
if (!mLastTarget.Equals(aNewTarget)) {
mLastTarget = aNewTarget;
return true;
}
return false;
}
//----------------------------------------------------------------------
// Implementation helpers
nsresult
nsSMILAnimationFunction::InterpolateResult(const nsSMILValueArray& aValues,
nsSMILValue& aResult,
nsSMILValue& 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()) {
nsSMILTime dur = mSimpleDuration.GetMillis();
NS_ABORT_IF_FALSE(dur >= 0, "Simple duration should not be negative");
NS_ABORT_IF_FALSE(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;
nsSMILCalcMode calcMode = GetCalcMode();
if (calcMode != CALC_DISCRETE) {
// Get the normalised progress between adjacent values
const nsSMILValue* from = nullptr;
const nsSMILValue* to = nullptr;
// Init to -1 to make sure that if we ever forget to set this, the
// NS_ABORT_IF_FALSE 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)) {
NS_ABORT_IF_FALSE(from, "NULL from-value during interpolation");
NS_ABORT_IF_FALSE(to, "NULL to-value during interpolation");
NS_ABORT_IF_FALSE(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];
}
rv = NS_OK;
}
return rv;
}
nsresult
nsSMILAnimationFunction::AccumulateResult(const nsSMILValueArray& aValues,
nsSMILValue& aResult)
{
if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) {
const nsSMILValue& 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
nsSMILAnimationFunction::ComputePacedPosition(const nsSMILValueArray& aValues,
double aSimpleProgress,
double& aIntervalProgress,
const nsSMILValue*& aFrom,
const nsSMILValue*& 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");
NS_ABORT_IF_FALSE(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;
#ifdef DEBUG
nsresult rv =
#endif
aValues[i].ComputeDistance(aValues[i+1], curIntervalDist);
NS_ABORT_IF_FALSE(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;
}
}
NS_NOTREACHED("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
nsSMILAnimationFunction::ComputePacedTotalDistance(
const nsSMILValueArray& 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
NS_ABORT_IF_FALSE(tmpDist >= 0.0f, "distance values must be non-negative");
tmpDist = std::max(tmpDist, 0.0);
totalDistance += tmpDist;
}
return totalDistance;
}
double
nsSMILAnimationFunction::ScaleSimpleProgress(double aProgress,
nsSMILCalcMode 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]) {
NS_ABORT_IF_FALSE(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
nsSMILAnimationFunction::ScaleIntervalProgress(double aProgress,
uint32_t aIntervalIndex)
{
if (GetCalcMode() != CALC_SPLINE)
return aProgress;
if (!HasAttr(nsGkAtoms::keySplines))
return aProgress;
NS_ABORT_IF_FALSE(aIntervalIndex < mKeySplines.Length(),
"Invalid interval index");
nsSMILKeySpline const &spline = mKeySplines[aIntervalIndex];
return spline.GetSplineValue(aProgress);
}
bool
nsSMILAnimationFunction::HasAttr(nsIAtom* aAttName) const
{
return mAnimationElement->HasAnimAttr(aAttName);
}
const nsAttrValue*
nsSMILAnimationFunction::GetAttr(nsIAtom* aAttName) const
{
return mAnimationElement->GetAnimAttr(aAttName);
}
bool
nsSMILAnimationFunction::GetAttr(nsIAtom* aAttName, nsAString& aResult) const
{
return mAnimationElement->GetAnimAttr(aAttName, aResult);
}
/*
* A utility function to make querying an attribute that corresponds to an
* nsSMILValue 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 nsSMILValue.
* @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 nsISMILAttr::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
nsSMILAnimationFunction::ParseAttr(nsIAtom* aAttName,
const nsISMILAttr& aSMILAttr,
nsSMILValue& aResult,
bool& aPreventCachingOfSandwich) const
{
nsAutoString attValue;
if (GetAttr(aAttName, attValue)) {
bool preventCachingOfSandwich = false;
nsresult rv = aSMILAttr.ValueFromString(attValue, mAnimationElement,
aResult, preventCachingOfSandwich);
if (NS_FAILED(rv))
return false;
if (preventCachingOfSandwich) {
aPreventCachingOfSandwich = true;
}
}
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
nsSMILAnimationFunction::GetValues(const nsISMILAttr& aSMILAttr,
nsSMILValueArray& aResult)
{
if (!mAnimationElement)
return NS_ERROR_FAILURE;
mValueNeedsReparsingEverySample = false;
nsSMILValueArray result;
// If "values" is set, use it
if (HasAttr(nsGkAtoms::values)) {
nsAutoString attValue;
GetAttr(nsGkAtoms::values, attValue);
bool preventCachingOfSandwich = false;
nsresult rv = nsSMILParserUtils::ParseValues(attValue, mAnimationElement,
aSMILAttr, result,
preventCachingOfSandwich);
if (NS_FAILED(rv))
return rv;
if (preventCachingOfSandwich) {
mValueNeedsReparsingEverySample = true;
}
// Else try to/from/by
} else {
bool preventCachingOfSandwich = false;
bool parseOk = true;
nsSMILValue 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)
return NS_ERROR_FAILURE;
result.SetCapacity(2);
if (!to.IsNull()) {
if (!from.IsNull()) {
result.AppendElement(from);
result.AppendElement(to);
} else {
result.AppendElement(to);
}
} else if (!by.IsNull()) {
nsSMILValue effectiveFrom(by.mType);
if (!from.IsNull())
effectiveFrom = from;
// Set values to 'from; from + by'
result.AppendElement(effectiveFrom);
nsSMILValue effectiveTo(effectiveFrom);
if (!effectiveTo.IsNull() && NS_SUCCEEDED(effectiveTo.Add(by))) {
result.AppendElement(effectiveTo);
} 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;
}
}
result.SwapElements(aResult);
return NS_OK;
}
void
nsSMILAnimationFunction::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
nsSMILAnimationFunction::CheckKeyTimes(uint32_t aNumValues)
{
if (!HasAttr(nsGkAtoms::keyTimes))
return;
nsSMILCalcMode 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
nsSMILAnimationFunction::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
nsSMILAnimationFunction::IsValueFixedForSimpleDuration() const
{
return mSimpleDuration.IsIndefinite() ||
(!mHasChanged && mPrevSampleWasSingleValueAnimation);
}
//----------------------------------------------------------------------
// Property getters
bool
nsSMILAnimationFunction::GetAccumulate() const
{
const nsAttrValue* value = GetAttr(nsGkAtoms::accumulate);
if (!value)
return false;
return value->GetEnumValue();
}
bool
nsSMILAnimationFunction::GetAdditive() const
{
const nsAttrValue* value = GetAttr(nsGkAtoms::additive);
if (!value)
return false;
return value->GetEnumValue();
}
nsSMILAnimationFunction::nsSMILCalcMode
nsSMILAnimationFunction::GetCalcMode() const
{
const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode);
if (!value)
return CALC_LINEAR;
return nsSMILCalcMode(value->GetEnumValue());
}
//----------------------------------------------------------------------
// Property setters / un-setters:
nsresult
nsSMILAnimationFunction::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
nsSMILAnimationFunction::UnsetAccumulate()
{
SetAccumulateErrorFlag(false);
mHasChanged = true;
}
nsresult
nsSMILAnimationFunction::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
nsSMILAnimationFunction::UnsetAdditive()
{
SetAdditiveErrorFlag(false);
mHasChanged = true;
}
nsresult
nsSMILAnimationFunction::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
nsSMILAnimationFunction::UnsetCalcMode()
{
SetCalcModeErrorFlag(false);
mHasChanged = true;
}
nsresult
nsSMILAnimationFunction::SetKeySplines(const nsAString& aKeySplines,
nsAttrValue& aResult)
{
mKeySplines.Clear();
aResult.SetTo(aKeySplines);
nsTArray<double> keySplines;
nsresult rv = nsSMILParserUtils::ParseKeySplines(aKeySplines, keySplines);
if (keySplines.Length() < 1 || keySplines.Length() % 4)
rv = NS_ERROR_FAILURE;
if (NS_SUCCEEDED(rv))
{
mKeySplines.SetCapacity(keySplines.Length() % 4);
for (uint32_t i = 0; i < keySplines.Length() && NS_SUCCEEDED(rv); i += 4)
{
if (!mKeySplines.AppendElement(nsSMILKeySpline(keySplines[i],
keySplines[i+1],
keySplines[i+2],
keySplines[i+3]))) {
rv = NS_ERROR_OUT_OF_MEMORY;
}
}
}
mHasChanged = true;
return rv;
}
void
nsSMILAnimationFunction::UnsetKeySplines()
{
mKeySplines.Clear();
SetKeySplinesErrorFlag(false);
mHasChanged = true;
}
nsresult
nsSMILAnimationFunction::SetKeyTimes(const nsAString& aKeyTimes,
nsAttrValue& aResult)
{
mKeyTimes.Clear();
aResult.SetTo(aKeyTimes);
nsresult rv =
nsSMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyTimes, true,
mKeyTimes);
if (NS_SUCCEEDED(rv) && mKeyTimes.Length() < 1)
rv = NS_ERROR_FAILURE;
if (NS_FAILED(rv))
mKeyTimes.Clear();
mHasChanged = true;
return NS_OK;
}
void
nsSMILAnimationFunction::UnsetKeyTimes()
{
mKeyTimes.Clear();
SetKeyTimesErrorFlag(false);
mHasChanged = true;
}