1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "nsSMILAnimationFunction.h"
8 
9 #include "mozilla/dom/SVGAnimationElement.h"
10 #include "mozilla/Move.h"
11 #include "nsISMILAttr.h"
12 #include "nsSMILCSSValueType.h"
13 #include "nsSMILParserUtils.h"
14 #include "nsSMILNullType.h"
15 #include "nsSMILTimedElement.h"
16 #include "nsAttrValueInlines.h"
17 #include "nsGkAtoms.h"
18 #include "nsCOMPtr.h"
19 #include "nsCOMArray.h"
20 #include "nsIContent.h"
21 #include "nsContentUtils.h"
22 #include "nsReadableUtils.h"
23 #include "nsString.h"
24 #include <math.h>
25 #include <algorithm>
26 
27 using namespace mozilla::dom;
28 
29 //----------------------------------------------------------------------
30 // Static members
31 
32 nsAttrValue::EnumTable nsSMILAnimationFunction::sAccumulateTable[] = {
33       {"none", false},
34       {"sum", true},
35       {nullptr, 0}
36 };
37 
38 nsAttrValue::EnumTable nsSMILAnimationFunction::sAdditiveTable[] = {
39       {"replace", false},
40       {"sum", true},
41       {nullptr, 0}
42 };
43 
44 nsAttrValue::EnumTable nsSMILAnimationFunction::sCalcModeTable[] = {
45       {"linear", CALC_LINEAR},
46       {"discrete", CALC_DISCRETE},
47       {"paced", CALC_PACED},
48       {"spline", CALC_SPLINE},
49       {nullptr, 0}
50 };
51 
52 // Any negative number should be fine as a sentinel here,
53 // because valid distances are non-negative.
54 #define COMPUTE_DISTANCE_ERROR (-1)
55 
56 //----------------------------------------------------------------------
57 // Constructors etc.
58 
nsSMILAnimationFunction()59 nsSMILAnimationFunction::nsSMILAnimationFunction()
60   : mSampleTime(-1),
61     mRepeatIteration(0),
62     mBeginTime(INT64_MIN),
63     mAnimationElement(nullptr),
64     mErrorFlags(0),
65     mIsActive(false),
66     mIsFrozen(false),
67     mLastValue(false),
68     mHasChanged(true),
69     mValueNeedsReparsingEverySample(false),
70     mPrevSampleWasSingleValueAnimation(false),
71     mWasSkippedInPrevSample(false)
72 {
73 }
74 
75 void
SetAnimationElement(SVGAnimationElement * aAnimationElement)76 nsSMILAnimationFunction::SetAnimationElement(
77     SVGAnimationElement* aAnimationElement)
78 {
79   mAnimationElement = aAnimationElement;
80 }
81 
82 bool
SetAttr(nsIAtom * aAttribute,const nsAString & aValue,nsAttrValue & aResult,nsresult * aParseResult)83 nsSMILAnimationFunction::SetAttr(nsIAtom* aAttribute, const nsAString& aValue,
84                                  nsAttrValue& aResult, nsresult* aParseResult)
85 {
86   bool foundMatch = true;
87   nsresult parseResult = NS_OK;
88 
89   // The attributes 'by', 'from', 'to', and 'values' may be parsed differently
90   // depending on the element & attribute we're animating.  So instead of
91   // parsing them now we re-parse them at every sample.
92   if (aAttribute == nsGkAtoms::by ||
93       aAttribute == nsGkAtoms::from ||
94       aAttribute == nsGkAtoms::to ||
95       aAttribute == nsGkAtoms::values) {
96     // We parse to, from, by, values at sample time.
97     // XXX Need to flag which attribute has changed and then when we parse it at
98     // sample time, report any errors and reset the flag
99     mHasChanged = true;
100     aResult.SetTo(aValue);
101   } else if (aAttribute == nsGkAtoms::accumulate) {
102     parseResult = SetAccumulate(aValue, aResult);
103   } else if (aAttribute == nsGkAtoms::additive) {
104     parseResult = SetAdditive(aValue, aResult);
105   } else if (aAttribute == nsGkAtoms::calcMode) {
106     parseResult = SetCalcMode(aValue, aResult);
107   } else if (aAttribute == nsGkAtoms::keyTimes) {
108     parseResult = SetKeyTimes(aValue, aResult);
109   } else if (aAttribute == nsGkAtoms::keySplines) {
110     parseResult = SetKeySplines(aValue, aResult);
111   } else {
112     foundMatch = false;
113   }
114 
115   if (foundMatch && aParseResult) {
116     *aParseResult = parseResult;
117   }
118 
119   return foundMatch;
120 }
121 
122 bool
UnsetAttr(nsIAtom * aAttribute)123 nsSMILAnimationFunction::UnsetAttr(nsIAtom* aAttribute)
124 {
125   bool foundMatch = true;
126 
127   if (aAttribute == nsGkAtoms::by ||
128       aAttribute == nsGkAtoms::from ||
129       aAttribute == nsGkAtoms::to ||
130       aAttribute == nsGkAtoms::values) {
131     mHasChanged = true;
132   } else if (aAttribute == nsGkAtoms::accumulate) {
133     UnsetAccumulate();
134   } else if (aAttribute == nsGkAtoms::additive) {
135     UnsetAdditive();
136   } else if (aAttribute == nsGkAtoms::calcMode) {
137     UnsetCalcMode();
138   } else if (aAttribute == nsGkAtoms::keyTimes) {
139     UnsetKeyTimes();
140   } else if (aAttribute == nsGkAtoms::keySplines) {
141     UnsetKeySplines();
142   } else {
143     foundMatch = false;
144   }
145 
146   return foundMatch;
147 }
148 
149 void
SampleAt(nsSMILTime aSampleTime,const nsSMILTimeValue & aSimpleDuration,uint32_t aRepeatIteration)150 nsSMILAnimationFunction::SampleAt(nsSMILTime aSampleTime,
151                                   const nsSMILTimeValue& aSimpleDuration,
152                                   uint32_t aRepeatIteration)
153 {
154   // * Update mHasChanged ("Might this sample be different from prev one?")
155   // Were we previously sampling a fill="freeze" final val? (We're not anymore.)
156   mHasChanged |= mLastValue;
157 
158   // Are we sampling at a new point in simple duration? And does that matter?
159   mHasChanged |=
160     (mSampleTime != aSampleTime || mSimpleDuration != aSimpleDuration) &&
161     !IsValueFixedForSimpleDuration();
162 
163   // Are we on a new repeat and accumulating across repeats?
164   if (!mErrorFlags) { // (can't call GetAccumulate() if we've had parse errors)
165     mHasChanged |= (mRepeatIteration != aRepeatIteration) && GetAccumulate();
166   }
167 
168   mSampleTime       = aSampleTime;
169   mSimpleDuration   = aSimpleDuration;
170   mRepeatIteration  = aRepeatIteration;
171   mLastValue        = false;
172 }
173 
174 void
SampleLastValue(uint32_t aRepeatIteration)175 nsSMILAnimationFunction::SampleLastValue(uint32_t aRepeatIteration)
176 {
177   if (mHasChanged || !mLastValue || mRepeatIteration != aRepeatIteration) {
178     mHasChanged = true;
179   }
180 
181   mRepeatIteration  = aRepeatIteration;
182   mLastValue        = true;
183 }
184 
185 void
Activate(nsSMILTime aBeginTime)186 nsSMILAnimationFunction::Activate(nsSMILTime aBeginTime)
187 {
188   mBeginTime = aBeginTime;
189   mIsActive = true;
190   mIsFrozen = false;
191   mHasChanged = true;
192 }
193 
194 void
Inactivate(bool aIsFrozen)195 nsSMILAnimationFunction::Inactivate(bool aIsFrozen)
196 {
197   mIsActive = false;
198   mIsFrozen = aIsFrozen;
199   mHasChanged = true;
200 }
201 
202 void
ComposeResult(const nsISMILAttr & aSMILAttr,nsSMILValue & aResult)203 nsSMILAnimationFunction::ComposeResult(const nsISMILAttr& aSMILAttr,
204                                        nsSMILValue& aResult)
205 {
206   mHasChanged = false;
207   mPrevSampleWasSingleValueAnimation = false;
208   mWasSkippedInPrevSample = false;
209 
210   // Skip animations that are inactive or in error
211   if (!IsActiveOrFrozen() || mErrorFlags != 0)
212     return;
213 
214   // Get the animation values
215   nsSMILValueArray values;
216   nsresult rv = GetValues(aSMILAttr, values);
217   if (NS_FAILED(rv))
218     return;
219 
220   // Check that we have the right number of keySplines and keyTimes
221   CheckValueListDependentAttrs(values.Length());
222   if (mErrorFlags != 0)
223     return;
224 
225   // If this interval is active, we must have a non-negative mSampleTime
226   MOZ_ASSERT(mSampleTime >= 0 || !mIsActive,
227              "Negative sample time for active animation");
228   MOZ_ASSERT(mSimpleDuration.IsResolved() || mLastValue,
229              "Unresolved simple duration for active or frozen animation");
230 
231   // If we want to add but don't have a base value then just fail outright.
232   // This can happen when we skipped getting the base value because there's an
233   // animation function in the sandwich that should replace it but that function
234   // failed unexpectedly.
235   bool isAdditive = IsAdditive();
236   if (isAdditive && aResult.IsNull())
237     return;
238 
239   nsSMILValue result;
240 
241   if (values.Length() == 1 && !IsToAnimation()) {
242 
243     // Single-valued animation
244     result = values[0];
245     mPrevSampleWasSingleValueAnimation = true;
246 
247   } else if (mLastValue) {
248 
249     // Sampling last value
250     const nsSMILValue& last = values[values.Length() - 1];
251     result = last;
252 
253     // See comment in AccumulateResult: to-animation does not accumulate
254     if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) {
255       // If the target attribute type doesn't support addition Add will
256       // fail leaving result = last
257       result.Add(last, mRepeatIteration);
258     }
259 
260   } else {
261 
262     // Interpolation
263     if (NS_FAILED(InterpolateResult(values, result, aResult)))
264       return;
265 
266     if (NS_FAILED(AccumulateResult(values, result)))
267       return;
268   }
269 
270   // If additive animation isn't required or isn't supported, set the value.
271   if (!isAdditive || NS_FAILED(aResult.SandwichAdd(result))) {
272     aResult = Move(result);
273   }
274 }
275 
276 int8_t
CompareTo(const nsSMILAnimationFunction * aOther) const277 nsSMILAnimationFunction::CompareTo(const nsSMILAnimationFunction* aOther) const
278 {
279   NS_ENSURE_TRUE(aOther, 0);
280 
281   NS_ASSERTION(aOther != this, "Trying to compare to self");
282 
283   // Inactive animations sort first
284   if (!IsActiveOrFrozen() && aOther->IsActiveOrFrozen())
285     return -1;
286 
287   if (IsActiveOrFrozen() && !aOther->IsActiveOrFrozen())
288     return 1;
289 
290   // Sort based on begin time
291   if (mBeginTime != aOther->GetBeginTime())
292     return mBeginTime > aOther->GetBeginTime() ? 1 : -1;
293 
294   // Next sort based on syncbase dependencies: the dependent element sorts after
295   // its syncbase
296   const nsSMILTimedElement& thisTimedElement =
297     mAnimationElement->TimedElement();
298   const nsSMILTimedElement& otherTimedElement =
299     aOther->mAnimationElement->TimedElement();
300   if (thisTimedElement.IsTimeDependent(otherTimedElement))
301     return 1;
302   if (otherTimedElement.IsTimeDependent(thisTimedElement))
303     return -1;
304 
305   // Animations that appear later in the document sort after those earlier in
306   // the document
307   MOZ_ASSERT(mAnimationElement != aOther->mAnimationElement,
308              "Two animations cannot have the same animation content element!");
309 
310   return (nsContentUtils::PositionIsBefore(mAnimationElement, aOther->mAnimationElement))
311           ? -1 : 1;
312 }
313 
314 bool
WillReplace() const315 nsSMILAnimationFunction::WillReplace() const
316 {
317   /*
318    * In IsAdditive() we don't consider to-animation to be additive as it is
319    * a special case that is dealt with differently in the compositing method.
320    * Here, however, we return FALSE for to-animation (i.e. it will NOT replace
321    * the underlying value) as it builds on the underlying value.
322    */
323   return !mErrorFlags && !(IsAdditive() || IsToAnimation());
324 }
325 
326 bool
HasChanged() const327 nsSMILAnimationFunction::HasChanged() const
328 {
329   return mHasChanged || mValueNeedsReparsingEverySample;
330 }
331 
332 bool
UpdateCachedTarget(const nsSMILTargetIdentifier & aNewTarget)333 nsSMILAnimationFunction::UpdateCachedTarget(
334   const nsSMILTargetIdentifier& aNewTarget)
335 {
336   if (!mLastTarget.Equals(aNewTarget)) {
337     mLastTarget = aNewTarget;
338     return true;
339   }
340   return false;
341 }
342 
343 //----------------------------------------------------------------------
344 // Implementation helpers
345 
346 nsresult
InterpolateResult(const nsSMILValueArray & aValues,nsSMILValue & aResult,nsSMILValue & aBaseValue)347 nsSMILAnimationFunction::InterpolateResult(const nsSMILValueArray& aValues,
348                                            nsSMILValue& aResult,
349                                            nsSMILValue& aBaseValue)
350 {
351   // Sanity check animation values
352   if ((!IsToAnimation() && aValues.Length() < 2) ||
353       (IsToAnimation()  && aValues.Length() != 1)) {
354     NS_ERROR("Unexpected number of values");
355     return NS_ERROR_FAILURE;
356   }
357 
358   if (IsToAnimation() && aBaseValue.IsNull()) {
359     return NS_ERROR_FAILURE;
360   }
361 
362   // Get the normalised progress through the simple duration.
363   //
364   // If we have an indefinite simple duration, just set the progress to be
365   // 0 which will give us the expected behaviour of the animation being fixed at
366   // its starting point.
367   double simpleProgress = 0.0;
368 
369   if (mSimpleDuration.IsDefinite()) {
370     nsSMILTime dur = mSimpleDuration.GetMillis();
371 
372     MOZ_ASSERT(dur >= 0, "Simple duration should not be negative");
373     MOZ_ASSERT(mSampleTime >= 0, "Sample time should not be negative");
374 
375     if (mSampleTime >= dur || mSampleTime < 0) {
376       NS_ERROR("Animation sampled outside interval");
377       return NS_ERROR_FAILURE;
378     }
379 
380     if (dur > 0) {
381       simpleProgress = (double)mSampleTime / dur;
382     } // else leave simpleProgress at 0.0 (e.g. if mSampleTime == dur == 0)
383   }
384 
385   nsresult rv = NS_OK;
386   nsSMILCalcMode calcMode = GetCalcMode();
387 
388   // Force discrete calcMode for visibility since StyleAnimationValue will
389   // try to interpolate it using the special clamping behavior defined for
390   // CSS.
391   if (nsSMILCSSValueType::PropertyFromValue(aValues[0])
392         == eCSSProperty_visibility) {
393     calcMode = CALC_DISCRETE;
394   }
395 
396   if (calcMode != CALC_DISCRETE) {
397     // Get the normalised progress between adjacent values
398     const nsSMILValue* from = nullptr;
399     const nsSMILValue* to = nullptr;
400     // Init to -1 to make sure that if we ever forget to set this, the
401     // MOZ_ASSERT that tests that intervalProgress is in range will fail.
402     double intervalProgress = -1.f;
403     if (IsToAnimation()) {
404       from = &aBaseValue;
405       to = &aValues[0];
406       if (calcMode == CALC_PACED) {
407         // Note: key[Times/Splines/Points] are ignored for calcMode="paced"
408         intervalProgress = simpleProgress;
409       } else {
410         double scaledSimpleProgress =
411           ScaleSimpleProgress(simpleProgress, calcMode);
412         intervalProgress = ScaleIntervalProgress(scaledSimpleProgress, 0);
413       }
414     } else if (calcMode == CALC_PACED) {
415       rv = ComputePacedPosition(aValues, simpleProgress,
416                                 intervalProgress, from, to);
417       // Note: If the above call fails, we'll skip the "from->Interpolate"
418       // call below, and we'll drop into the CALC_DISCRETE section
419       // instead. (as the spec says we should, because our failure was
420       // presumably due to the values being non-additive)
421     } else { // calcMode == CALC_LINEAR or calcMode == CALC_SPLINE
422       double scaledSimpleProgress =
423         ScaleSimpleProgress(simpleProgress, calcMode);
424       uint32_t index = (uint32_t)floor(scaledSimpleProgress *
425                                        (aValues.Length() - 1));
426       from = &aValues[index];
427       to = &aValues[index + 1];
428       intervalProgress =
429         scaledSimpleProgress * (aValues.Length() - 1) - index;
430       intervalProgress = ScaleIntervalProgress(intervalProgress, index);
431     }
432 
433     if (NS_SUCCEEDED(rv)) {
434       MOZ_ASSERT(from, "NULL from-value during interpolation");
435       MOZ_ASSERT(to, "NULL to-value during interpolation");
436       MOZ_ASSERT(0.0f <= intervalProgress && intervalProgress < 1.0f,
437                  "Interval progress should be in the range [0, 1)");
438       rv = from->Interpolate(*to, intervalProgress, aResult);
439     }
440   }
441 
442   // Discrete-CalcMode case
443   // Note: If interpolation failed (isn't supported for this type), the SVG
444   // spec says to force discrete mode.
445   if (calcMode == CALC_DISCRETE || NS_FAILED(rv)) {
446     double scaledSimpleProgress =
447       ScaleSimpleProgress(simpleProgress, CALC_DISCRETE);
448 
449     // Floating-point errors can mean that, for example, a sample time of 29s in
450     // a 100s duration animation gives us a simple progress of 0.28999999999
451     // instead of the 0.29 we'd expect. Normally this isn't a noticeable
452     // problem, but when we have sudden jumps in animation values (such as is
453     // the case here with discrete animation) we can get unexpected results.
454     //
455     // To counteract this, before we perform a floor() on the animation
456     // progress, we add a tiny fudge factor to push us into the correct interval
457     // in cases where floating-point errors might cause us to fall short.
458     static const double kFloatingPointFudgeFactor = 1.0e-16;
459     if (scaledSimpleProgress + kFloatingPointFudgeFactor <= 1.0) {
460       scaledSimpleProgress += kFloatingPointFudgeFactor;
461     }
462 
463     if (IsToAnimation()) {
464       // We don't follow SMIL 3, 12.6.4, where discrete to animations
465       // are the same as <set> animations.  Instead, we treat it as a
466       // discrete animation with two values (the underlying value and
467       // the to="" value), and honor keyTimes="" as well.
468       uint32_t index = (uint32_t)floor(scaledSimpleProgress * 2);
469       aResult = index == 0 ? aBaseValue : aValues[0];
470     } else {
471       uint32_t index = (uint32_t)floor(scaledSimpleProgress * aValues.Length());
472       aResult = aValues[index];
473     }
474     rv = NS_OK;
475   }
476   return rv;
477 }
478 
479 nsresult
AccumulateResult(const nsSMILValueArray & aValues,nsSMILValue & aResult)480 nsSMILAnimationFunction::AccumulateResult(const nsSMILValueArray& aValues,
481                                           nsSMILValue& aResult)
482 {
483   if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) {
484     const nsSMILValue& lastValue = aValues[aValues.Length() - 1];
485 
486     // If the target attribute type doesn't support addition, Add will
487     // fail and we leave aResult untouched.
488     aResult.Add(lastValue, mRepeatIteration);
489   }
490 
491   return NS_OK;
492 }
493 
494 /*
495  * Given the simple progress for a paced animation, this method:
496  *  - determines which two elements of the values array we're in between
497  *    (returned as aFrom and aTo)
498  *  - determines where we are between them
499  *    (returned as aIntervalProgress)
500  *
501  * Returns NS_OK, or NS_ERROR_FAILURE if our values don't support distance
502  * computation.
503  */
504 nsresult
ComputePacedPosition(const nsSMILValueArray & aValues,double aSimpleProgress,double & aIntervalProgress,const nsSMILValue * & aFrom,const nsSMILValue * & aTo)505 nsSMILAnimationFunction::ComputePacedPosition(const nsSMILValueArray& aValues,
506                                               double aSimpleProgress,
507                                               double& aIntervalProgress,
508                                               const nsSMILValue*& aFrom,
509                                               const nsSMILValue*& aTo)
510 {
511   NS_ASSERTION(0.0f <= aSimpleProgress && aSimpleProgress < 1.0f,
512                "aSimpleProgress is out of bounds");
513   NS_ASSERTION(GetCalcMode() == CALC_PACED,
514                "Calling paced-specific function, but not in paced mode");
515   MOZ_ASSERT(aValues.Length() >= 2, "Unexpected number of values");
516 
517   // Trivial case: If we have just 2 values, then there's only one interval
518   // for us to traverse, and our progress across that interval is the exact
519   // same as our overall progress.
520   if (aValues.Length() == 2) {
521     aIntervalProgress = aSimpleProgress;
522     aFrom = &aValues[0];
523     aTo = &aValues[1];
524     return NS_OK;
525   }
526 
527   double totalDistance = ComputePacedTotalDistance(aValues);
528   if (totalDistance == COMPUTE_DISTANCE_ERROR)
529     return NS_ERROR_FAILURE;
530 
531   // If we have 0 total distance, then it's unclear where our "paced" position
532   // should be.  We can just fail, which drops us into discrete animation mode.
533   // (That's fine, since our values are apparently indistinguishable anyway.)
534   if (totalDistance == 0.0) {
535     return NS_ERROR_FAILURE;
536   }
537 
538   // total distance we should have moved at this point in time.
539   // (called 'remainingDist' due to how it's used in loop below)
540   double remainingDist = aSimpleProgress * totalDistance;
541 
542   // Must be satisfied, because totalDistance is a sum of (non-negative)
543   // distances, and aSimpleProgress is non-negative
544   NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative");
545 
546   // Find where remainingDist puts us in the list of values
547   // Note: We could optimize this next loop by caching the
548   // interval-distances in an array, but maybe that's excessive.
549   for (uint32_t i = 0; i < aValues.Length() - 1; i++) {
550     // Note: The following assertion is valid because remainingDist should
551     // start out non-negative, and this loop never shaves off more than its
552     // current value.
553     NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative");
554 
555     double curIntervalDist;
556 
557 #ifdef DEBUG
558     nsresult rv =
559 #endif
560       aValues[i].ComputeDistance(aValues[i+1], curIntervalDist);
561     MOZ_ASSERT(NS_SUCCEEDED(rv),
562                "If we got through ComputePacedTotalDistance, we should "
563                "be able to recompute each sub-distance without errors");
564 
565     NS_ASSERTION(curIntervalDist >= 0, "distance values must be non-negative");
566     // Clamp distance value at 0, just in case ComputeDistance is evil.
567     curIntervalDist = std::max(curIntervalDist, 0.0);
568 
569     if (remainingDist >= curIntervalDist) {
570       remainingDist -= curIntervalDist;
571     } else {
572       // NOTE: If we get here, then curIntervalDist necessarily is not 0. Why?
573       // Because this clause is only hit when remainingDist < curIntervalDist,
574       // and if curIntervalDist were 0, that would mean remainingDist would
575       // have to be < 0.  But that can't happen, because remainingDist (as
576       // a distance) is non-negative by definition.
577       NS_ASSERTION(curIntervalDist != 0,
578                    "We should never get here with this set to 0...");
579 
580       // We found the right spot -- an interpolated position between
581       // values i and i+1.
582       aFrom = &aValues[i];
583       aTo = &aValues[i+1];
584       aIntervalProgress = remainingDist / curIntervalDist;
585       return NS_OK;
586     }
587   }
588 
589   NS_NOTREACHED("shouldn't complete loop & get here -- if we do, "
590                 "then aSimpleProgress was probably out of bounds");
591   return NS_ERROR_FAILURE;
592 }
593 
594 /*
595  * Computes the total distance to be travelled by a paced animation.
596  *
597  * Returns the total distance, or returns COMPUTE_DISTANCE_ERROR if
598  * our values don't support distance computation.
599  */
600 double
ComputePacedTotalDistance(const nsSMILValueArray & aValues) const601 nsSMILAnimationFunction::ComputePacedTotalDistance(
602     const nsSMILValueArray& aValues) const
603 {
604   NS_ASSERTION(GetCalcMode() == CALC_PACED,
605                "Calling paced-specific function, but not in paced mode");
606 
607   double totalDistance = 0.0;
608   for (uint32_t i = 0; i < aValues.Length() - 1; i++) {
609     double tmpDist;
610     nsresult rv = aValues[i].ComputeDistance(aValues[i+1], tmpDist);
611     if (NS_FAILED(rv)) {
612       return COMPUTE_DISTANCE_ERROR;
613     }
614 
615     // Clamp distance value to 0, just in case we have an evil ComputeDistance
616     // implementation somewhere
617     MOZ_ASSERT(tmpDist >= 0.0f, "distance values must be non-negative");
618     tmpDist = std::max(tmpDist, 0.0);
619 
620     totalDistance += tmpDist;
621   }
622 
623   return totalDistance;
624 }
625 
626 double
ScaleSimpleProgress(double aProgress,nsSMILCalcMode aCalcMode)627 nsSMILAnimationFunction::ScaleSimpleProgress(double aProgress,
628                                              nsSMILCalcMode aCalcMode)
629 {
630   if (!HasAttr(nsGkAtoms::keyTimes))
631     return aProgress;
632 
633   uint32_t numTimes = mKeyTimes.Length();
634 
635   if (numTimes < 2)
636     return aProgress;
637 
638   uint32_t i = 0;
639   for (; i < numTimes - 2 && aProgress >= mKeyTimes[i+1]; ++i) { }
640 
641   if (aCalcMode == CALC_DISCRETE) {
642     // discrete calcMode behaviour differs in that each keyTime defines the time
643     // from when the corresponding value is set, and therefore the last value
644     // needn't be 1. So check if we're in the last 'interval', that is, the
645     // space between the final value and 1.0.
646     if (aProgress >= mKeyTimes[i+1]) {
647       MOZ_ASSERT(i == numTimes - 2,
648                  "aProgress is not in range of the current interval, yet the "
649                  "current interval is not the last bounded interval either.");
650       ++i;
651     }
652     return (double)i / numTimes;
653   }
654 
655   double& intervalStart = mKeyTimes[i];
656   double& intervalEnd   = mKeyTimes[i+1];
657 
658   double intervalLength = intervalEnd - intervalStart;
659   if (intervalLength <= 0.0)
660     return intervalStart;
661 
662   return (i + (aProgress - intervalStart) / intervalLength) /
663          double(numTimes - 1);
664 }
665 
666 double
ScaleIntervalProgress(double aProgress,uint32_t aIntervalIndex)667 nsSMILAnimationFunction::ScaleIntervalProgress(double aProgress,
668                                                uint32_t aIntervalIndex)
669 {
670   if (GetCalcMode() != CALC_SPLINE)
671     return aProgress;
672 
673   if (!HasAttr(nsGkAtoms::keySplines))
674     return aProgress;
675 
676   MOZ_ASSERT(aIntervalIndex < mKeySplines.Length(),
677              "Invalid interval index");
678 
679   nsSMILKeySpline const &spline = mKeySplines[aIntervalIndex];
680   return spline.GetSplineValue(aProgress);
681 }
682 
683 bool
HasAttr(nsIAtom * aAttName) const684 nsSMILAnimationFunction::HasAttr(nsIAtom* aAttName) const
685 {
686   return mAnimationElement->HasAnimAttr(aAttName);
687 }
688 
689 const nsAttrValue*
GetAttr(nsIAtom * aAttName) const690 nsSMILAnimationFunction::GetAttr(nsIAtom* aAttName) const
691 {
692   return mAnimationElement->GetAnimAttr(aAttName);
693 }
694 
695 bool
GetAttr(nsIAtom * aAttName,nsAString & aResult) const696 nsSMILAnimationFunction::GetAttr(nsIAtom* aAttName, nsAString& aResult) const
697 {
698   return mAnimationElement->GetAnimAttr(aAttName, aResult);
699 }
700 
701 /*
702  * A utility function to make querying an attribute that corresponds to an
703  * nsSMILValue a little neater.
704  *
705  * @param aAttName    The attribute name (in the global namespace).
706  * @param aSMILAttr   The SMIL attribute to perform the parsing.
707  * @param[out] aResult        The resulting nsSMILValue.
708  * @param[out] aPreventCachingOfSandwich
709  *                    If |aResult| contains dependencies on its context that
710  *                    should prevent the result of the animation sandwich from
711  *                    being cached and reused in future samples (as reported
712  *                    by nsISMILAttr::ValueFromString), then this outparam
713  *                    will be set to true. Otherwise it is left unmodified.
714  *
715  * Returns false if a parse error occurred, otherwise returns true.
716  */
717 bool
ParseAttr(nsIAtom * aAttName,const nsISMILAttr & aSMILAttr,nsSMILValue & aResult,bool & aPreventCachingOfSandwich) const718 nsSMILAnimationFunction::ParseAttr(nsIAtom* aAttName,
719                                    const nsISMILAttr& aSMILAttr,
720                                    nsSMILValue& aResult,
721                                    bool& aPreventCachingOfSandwich) const
722 {
723   nsAutoString attValue;
724   if (GetAttr(aAttName, attValue)) {
725     bool preventCachingOfSandwich = false;
726     nsresult rv = aSMILAttr.ValueFromString(attValue, mAnimationElement,
727                                             aResult, preventCachingOfSandwich);
728     if (NS_FAILED(rv))
729       return false;
730 
731     if (preventCachingOfSandwich) {
732       aPreventCachingOfSandwich = true;
733     }
734   }
735   return true;
736 }
737 
738 /*
739  * SMILANIM specifies the following rules for animation function values:
740  *
741  * (1) if values is set, it overrides everything
742  * (2) for from/to/by animation at least to or by must be specified, from on its
743  *     own (or nothing) is an error--which we will ignore
744  * (3) if both by and to are specified only to will be used, by will be ignored
745  * (4) if by is specified without from (by animation), forces additive behaviour
746  * (5) if to is specified without from (to animation), special care needs to be
747  *     taken when compositing animation as such animations are composited last.
748  *
749  * This helper method applies these rules to fill in the values list and to set
750  * some internal state.
751  */
752 nsresult
GetValues(const nsISMILAttr & aSMILAttr,nsSMILValueArray & aResult)753 nsSMILAnimationFunction::GetValues(const nsISMILAttr& aSMILAttr,
754                                    nsSMILValueArray& aResult)
755 {
756   if (!mAnimationElement)
757     return NS_ERROR_FAILURE;
758 
759   mValueNeedsReparsingEverySample = false;
760   nsSMILValueArray result;
761 
762   // If "values" is set, use it
763   if (HasAttr(nsGkAtoms::values)) {
764     nsAutoString attValue;
765     GetAttr(nsGkAtoms::values, attValue);
766     bool preventCachingOfSandwich = false;
767     if (!nsSMILParserUtils::ParseValues(attValue, mAnimationElement,
768                                         aSMILAttr, result,
769                                         preventCachingOfSandwich)) {
770       return NS_ERROR_FAILURE;
771     }
772 
773     if (preventCachingOfSandwich) {
774       mValueNeedsReparsingEverySample = true;
775     }
776   // Else try to/from/by
777   } else {
778     bool preventCachingOfSandwich = false;
779     bool parseOk = true;
780     nsSMILValue to, from, by;
781     parseOk &= ParseAttr(nsGkAtoms::to,   aSMILAttr, to,
782                          preventCachingOfSandwich);
783     parseOk &= ParseAttr(nsGkAtoms::from, aSMILAttr, from,
784                          preventCachingOfSandwich);
785     parseOk &= ParseAttr(nsGkAtoms::by,   aSMILAttr, by,
786                          preventCachingOfSandwich);
787 
788     if (preventCachingOfSandwich) {
789       mValueNeedsReparsingEverySample = true;
790     }
791 
792     if (!parseOk || !result.SetCapacity(2, mozilla::fallible)) {
793       return NS_ERROR_FAILURE;
794     }
795 
796     // AppendElement() below must succeed, because SetCapacity() succeeded.
797     if (!to.IsNull()) {
798       if (!from.IsNull()) {
799         MOZ_ALWAYS_TRUE(result.AppendElement(from, mozilla::fallible));
800         MOZ_ALWAYS_TRUE(result.AppendElement(to, mozilla::fallible));
801       } else {
802         MOZ_ALWAYS_TRUE(result.AppendElement(to, mozilla::fallible));
803       }
804     } else if (!by.IsNull()) {
805       nsSMILValue effectiveFrom(by.mType);
806       if (!from.IsNull())
807         effectiveFrom = from;
808       // Set values to 'from; from + by'
809       MOZ_ALWAYS_TRUE(result.AppendElement(effectiveFrom, mozilla::fallible));
810       nsSMILValue effectiveTo(effectiveFrom);
811       if (!effectiveTo.IsNull() && NS_SUCCEEDED(effectiveTo.Add(by))) {
812         MOZ_ALWAYS_TRUE(result.AppendElement(effectiveTo, mozilla::fallible));
813       } else {
814         // Using by-animation with non-additive type or bad base-value
815         return NS_ERROR_FAILURE;
816       }
817     } else {
818       // No values, no to, no by -- call it a day
819       return NS_ERROR_FAILURE;
820     }
821   }
822 
823   result.SwapElements(aResult);
824 
825   return NS_OK;
826 }
827 
828 void
CheckValueListDependentAttrs(uint32_t aNumValues)829 nsSMILAnimationFunction::CheckValueListDependentAttrs(uint32_t aNumValues)
830 {
831   CheckKeyTimes(aNumValues);
832   CheckKeySplines(aNumValues);
833 }
834 
835 /**
836  * Performs checks for the keyTimes attribute required by the SMIL spec but
837  * which depend on other attributes and therefore needs to be updated as
838  * dependent attributes are set.
839  */
840 void
CheckKeyTimes(uint32_t aNumValues)841 nsSMILAnimationFunction::CheckKeyTimes(uint32_t aNumValues)
842 {
843   if (!HasAttr(nsGkAtoms::keyTimes))
844     return;
845 
846   nsSMILCalcMode calcMode = GetCalcMode();
847 
848   // attribute is ignored for calcMode = paced
849   if (calcMode == CALC_PACED) {
850     SetKeyTimesErrorFlag(false);
851     return;
852   }
853 
854   uint32_t numKeyTimes = mKeyTimes.Length();
855   if (numKeyTimes < 1) {
856     // keyTimes isn't set or failed preliminary checks
857     SetKeyTimesErrorFlag(true);
858     return;
859   }
860 
861   // no. keyTimes == no. values
862   // For to-animation the number of values is considered to be 2.
863   bool matchingNumOfValues =
864     numKeyTimes == (IsToAnimation() ? 2 : aNumValues);
865   if (!matchingNumOfValues) {
866     SetKeyTimesErrorFlag(true);
867     return;
868   }
869 
870   // first value must be 0
871   if (mKeyTimes[0] != 0.0) {
872     SetKeyTimesErrorFlag(true);
873     return;
874   }
875 
876   // last value must be 1 for linear or spline calcModes
877   if (calcMode != CALC_DISCRETE && numKeyTimes > 1 &&
878       mKeyTimes[numKeyTimes - 1] != 1.0) {
879     SetKeyTimesErrorFlag(true);
880     return;
881   }
882 
883   SetKeyTimesErrorFlag(false);
884 }
885 
886 void
CheckKeySplines(uint32_t aNumValues)887 nsSMILAnimationFunction::CheckKeySplines(uint32_t aNumValues)
888 {
889   // attribute is ignored if calc mode is not spline
890   if (GetCalcMode() != CALC_SPLINE) {
891     SetKeySplinesErrorFlag(false);
892     return;
893   }
894 
895   // calc mode is spline but the attribute is not set
896   if (!HasAttr(nsGkAtoms::keySplines)) {
897     SetKeySplinesErrorFlag(false);
898     return;
899   }
900 
901   if (mKeySplines.Length() < 1) {
902     // keyTimes isn't set or failed preliminary checks
903     SetKeySplinesErrorFlag(true);
904     return;
905   }
906 
907   // ignore splines if there's only one value
908   if (aNumValues == 1 && !IsToAnimation()) {
909     SetKeySplinesErrorFlag(false);
910     return;
911   }
912 
913   // no. keySpline specs == no. values - 1
914   uint32_t splineSpecs = mKeySplines.Length();
915   if ((splineSpecs != aNumValues - 1 && !IsToAnimation()) ||
916       (IsToAnimation() && splineSpecs != 1)) {
917     SetKeySplinesErrorFlag(true);
918     return;
919   }
920 
921   SetKeySplinesErrorFlag(false);
922 }
923 
924 bool
IsValueFixedForSimpleDuration() const925 nsSMILAnimationFunction::IsValueFixedForSimpleDuration() const
926 {
927   return mSimpleDuration.IsIndefinite() ||
928     (!mHasChanged && mPrevSampleWasSingleValueAnimation);
929 }
930 
931 //----------------------------------------------------------------------
932 // Property getters
933 
934 bool
GetAccumulate() const935 nsSMILAnimationFunction::GetAccumulate() const
936 {
937   const nsAttrValue* value = GetAttr(nsGkAtoms::accumulate);
938   if (!value)
939     return false;
940 
941   return value->GetEnumValue();
942 }
943 
944 bool
GetAdditive() const945 nsSMILAnimationFunction::GetAdditive() const
946 {
947   const nsAttrValue* value = GetAttr(nsGkAtoms::additive);
948   if (!value)
949     return false;
950 
951   return value->GetEnumValue();
952 }
953 
954 nsSMILAnimationFunction::nsSMILCalcMode
GetCalcMode() const955 nsSMILAnimationFunction::GetCalcMode() const
956 {
957   const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode);
958   if (!value)
959     return CALC_LINEAR;
960 
961   return nsSMILCalcMode(value->GetEnumValue());
962 }
963 
964 //----------------------------------------------------------------------
965 // Property setters / un-setters:
966 
967 nsresult
SetAccumulate(const nsAString & aAccumulate,nsAttrValue & aResult)968 nsSMILAnimationFunction::SetAccumulate(const nsAString& aAccumulate,
969                                        nsAttrValue& aResult)
970 {
971   mHasChanged = true;
972   bool parseResult =
973     aResult.ParseEnumValue(aAccumulate, sAccumulateTable, true);
974   SetAccumulateErrorFlag(!parseResult);
975   return parseResult ? NS_OK : NS_ERROR_FAILURE;
976 }
977 
978 void
UnsetAccumulate()979 nsSMILAnimationFunction::UnsetAccumulate()
980 {
981   SetAccumulateErrorFlag(false);
982   mHasChanged = true;
983 }
984 
985 nsresult
SetAdditive(const nsAString & aAdditive,nsAttrValue & aResult)986 nsSMILAnimationFunction::SetAdditive(const nsAString& aAdditive,
987                                      nsAttrValue& aResult)
988 {
989   mHasChanged = true;
990   bool parseResult
991     = aResult.ParseEnumValue(aAdditive, sAdditiveTable, true);
992   SetAdditiveErrorFlag(!parseResult);
993   return parseResult ? NS_OK : NS_ERROR_FAILURE;
994 }
995 
996 void
UnsetAdditive()997 nsSMILAnimationFunction::UnsetAdditive()
998 {
999   SetAdditiveErrorFlag(false);
1000   mHasChanged = true;
1001 }
1002 
1003 nsresult
SetCalcMode(const nsAString & aCalcMode,nsAttrValue & aResult)1004 nsSMILAnimationFunction::SetCalcMode(const nsAString& aCalcMode,
1005                                      nsAttrValue& aResult)
1006 {
1007   mHasChanged = true;
1008   bool parseResult
1009     = aResult.ParseEnumValue(aCalcMode, sCalcModeTable, true);
1010   SetCalcModeErrorFlag(!parseResult);
1011   return parseResult ? NS_OK : NS_ERROR_FAILURE;
1012 }
1013 
1014 void
UnsetCalcMode()1015 nsSMILAnimationFunction::UnsetCalcMode()
1016 {
1017   SetCalcModeErrorFlag(false);
1018   mHasChanged = true;
1019 }
1020 
1021 nsresult
SetKeySplines(const nsAString & aKeySplines,nsAttrValue & aResult)1022 nsSMILAnimationFunction::SetKeySplines(const nsAString& aKeySplines,
1023                                        nsAttrValue& aResult)
1024 {
1025   mKeySplines.Clear();
1026   aResult.SetTo(aKeySplines);
1027 
1028   mHasChanged = true;
1029 
1030   if (!nsSMILParserUtils::ParseKeySplines(aKeySplines, mKeySplines)) {
1031     mKeySplines.Clear();
1032     return NS_ERROR_FAILURE;
1033   }
1034 
1035   return NS_OK;
1036 }
1037 
1038 void
UnsetKeySplines()1039 nsSMILAnimationFunction::UnsetKeySplines()
1040 {
1041   mKeySplines.Clear();
1042   SetKeySplinesErrorFlag(false);
1043   mHasChanged = true;
1044 }
1045 
1046 nsresult
SetKeyTimes(const nsAString & aKeyTimes,nsAttrValue & aResult)1047 nsSMILAnimationFunction::SetKeyTimes(const nsAString& aKeyTimes,
1048                                      nsAttrValue& aResult)
1049 {
1050   mKeyTimes.Clear();
1051   aResult.SetTo(aKeyTimes);
1052 
1053   mHasChanged = true;
1054 
1055   if (!nsSMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyTimes, true,
1056                                                               mKeyTimes)) {
1057     mKeyTimes.Clear();
1058     return NS_ERROR_FAILURE;
1059   }
1060 
1061   return NS_OK;
1062 }
1063 
1064 void
UnsetKeyTimes()1065 nsSMILAnimationFunction::UnsetKeyTimes()
1066 {
1067   mKeyTimes.Clear();
1068   SetKeyTimesErrorFlag(false);
1069   mHasChanged = true;
1070 }
1071