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