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 /* implementation of nsISMILType for use by <animateMotion> element */
8 
9 #include "SVGMotionSMILType.h"
10 
11 #include "gfx2DGlue.h"
12 #include "mozilla/gfx/Point.h"
13 #include "nsSMILValue.h"
14 #include "nsDebug.h"
15 #include "nsMathUtils.h"
16 #include "nsISupportsUtils.h"
17 #include "nsTArray.h"
18 #include <math.h>
19 
20 using namespace mozilla::gfx;
21 
22 namespace mozilla {
23 
24 /*static*/ SVGMotionSMILType SVGMotionSMILType::sSingleton;
25 
26 
27 // Helper enum, for distinguishing between types of MotionSegment structs
28 enum SegmentType {
29   eSegmentType_Translation,
30   eSegmentType_PathPoint
31 };
32 
33 // Helper Structs: containers for params to define our MotionSegment
34 // (either simple translation or point-on-a-path)
35 struct TranslationParams {  // Simple translation
36   float mX;
37   float mY;
38 };
39 struct PathPointParams {  // Point along a path
40   // Refcounted: need to AddRef/Release.  This can't be an nsRefPtr because
41   // this struct is used inside a union so it can't have a default constructor.
42   Path* MOZ_OWNING_REF mPath;
43   float mDistToPoint; // Distance from path start to the point on the path that
44                       // we're interested in.
45 };
46 
47 /**
48  * Helper Struct: MotionSegment
49  *
50  * Instances of this class represent the points that we move between during
51  * <animateMotion>.  Each nsSMILValue will get a nsTArray of these (generally
52  * with at most 1 entry in the array, except for in SandwichAdd).  (This
53  * matches our behavior in nsSVGTransformSMILType.)
54  *
55  * NOTE: In general, MotionSegments are represented as points on a path
56  * (eSegmentType_PathPoint), so that we can easily interpolate and compute
57  * distance *along their path*.  However, Add() outputs MotionSegments as
58  * simple translations (eSegmentType_Translation), because adding two points
59  * from a path (e.g. when accumulating a repeated animation) will generally
60  * take you to an arbitrary point *off* of the path.
61  */
62 struct MotionSegment
63 {
64   // Default constructor just locks us into being a Translation, and leaves
65   // other fields uninitialized (since client is presumably about to set them)
MotionSegmentmozilla::MotionSegment66   MotionSegment()
67     : mSegmentType(eSegmentType_Translation)
68   { }
69 
70   // Constructor for a translation
MotionSegmentmozilla::MotionSegment71   MotionSegment(float aX, float aY, float aRotateAngle)
72     : mRotateType(eRotateType_Explicit), mRotateAngle(aRotateAngle),
73       mSegmentType(eSegmentType_Translation)
74   {
75     mU.mTranslationParams.mX = aX;
76     mU.mTranslationParams.mY = aY;
77   }
78 
79   // Constructor for a point on a path (NOTE: AddRef's)
MotionSegmentmozilla::MotionSegment80   MotionSegment(Path* aPath, float aDistToPoint,
81                 RotateType aRotateType, float aRotateAngle)
82     : mRotateType(aRotateType), mRotateAngle(aRotateAngle),
83       mSegmentType(eSegmentType_PathPoint)
84   {
85     mU.mPathPointParams.mPath = aPath;
86     mU.mPathPointParams.mDistToPoint = aDistToPoint;
87 
88     NS_ADDREF(mU.mPathPointParams.mPath); // Retain a reference to path
89   }
90 
91   // Copy constructor (NOTE: AddRef's if we're eSegmentType_PathPoint)
MotionSegmentmozilla::MotionSegment92   MotionSegment(const MotionSegment& aOther)
93     : mRotateType(aOther.mRotateType), mRotateAngle(aOther.mRotateAngle),
94       mSegmentType(aOther.mSegmentType)
95   {
96     if (mSegmentType == eSegmentType_Translation) {
97       mU.mTranslationParams = aOther.mU.mTranslationParams;
98     } else { // mSegmentType == eSegmentType_PathPoint
99       mU.mPathPointParams = aOther.mU.mPathPointParams;
100       NS_ADDREF(mU.mPathPointParams.mPath); // Retain a reference to path
101     }
102   }
103 
104   // Destructor (releases any reference we were holding onto)
~MotionSegmentmozilla::MotionSegment105   ~MotionSegment()
106   {
107     if (mSegmentType == eSegmentType_PathPoint) {
108       NS_RELEASE(mU.mPathPointParams.mPath);
109     }
110   }
111 
112   // Comparison operators
operator ==mozilla::MotionSegment113   bool operator==(const MotionSegment& aOther) const
114   {
115     // Compare basic params
116     if (mSegmentType != aOther.mSegmentType ||
117         mRotateType  != aOther.mRotateType ||
118         (mRotateType == eRotateType_Explicit &&  // Technically, angle mismatch
119          mRotateAngle != aOther.mRotateAngle)) { // only matters for Explicit.
120       return false;
121     }
122 
123     // Compare translation params, if we're a translation.
124     if (mSegmentType == eSegmentType_Translation) {
125       return mU.mTranslationParams.mX == aOther.mU.mTranslationParams.mX &&
126              mU.mTranslationParams.mY == aOther.mU.mTranslationParams.mY;
127     }
128 
129     // Else, compare path-point params, if we're a path point.
130     return (mU.mPathPointParams.mPath == aOther.mU.mPathPointParams.mPath) &&
131       (mU.mPathPointParams.mDistToPoint ==
132        aOther.mU.mPathPointParams.mDistToPoint);
133   }
134 
operator !=mozilla::MotionSegment135   bool operator!=(const MotionSegment& aOther) const
136   {
137     return !(*this == aOther);
138   }
139 
140   // Member Data
141   // -----------
142   RotateType mRotateType; // Explicit angle vs. auto vs. auto-reverse.
143   float mRotateAngle;     // Only used if mRotateType == eRotateType_Explicit.
144   const SegmentType mSegmentType; // This determines how we interpret
145                                   // mU. (const for safety/sanity)
146 
147   union { // Union to let us hold the params for either segment-type.
148     TranslationParams mTranslationParams;
149     PathPointParams mPathPointParams;
150   } mU;
151 };
152 
153 typedef FallibleTArray<MotionSegment> MotionSegmentArray;
154 
155 // Helper methods to cast nsSMILValue.mU.mPtr to the right pointer-type
156 static MotionSegmentArray&
ExtractMotionSegmentArray(nsSMILValue & aValue)157 ExtractMotionSegmentArray(nsSMILValue& aValue)
158 {
159   return *static_cast<MotionSegmentArray*>(aValue.mU.mPtr);
160 }
161 
162 static const MotionSegmentArray&
ExtractMotionSegmentArray(const nsSMILValue & aValue)163 ExtractMotionSegmentArray(const nsSMILValue& aValue)
164 {
165   return *static_cast<const MotionSegmentArray*>(aValue.mU.mPtr);
166 }
167 
168 // nsISMILType Methods
169 // -------------------
170 
171 void
Init(nsSMILValue & aValue) const172 SVGMotionSMILType::Init(nsSMILValue& aValue) const
173 {
174   MOZ_ASSERT(aValue.IsNull(), "Unexpected SMIL type");
175 
176   aValue.mType = this;
177   aValue.mU.mPtr = new MotionSegmentArray(1);
178 }
179 
180 void
Destroy(nsSMILValue & aValue) const181 SVGMotionSMILType::Destroy(nsSMILValue& aValue) const
182 {
183   MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL type");
184 
185   MotionSegmentArray* arr = static_cast<MotionSegmentArray*>(aValue.mU.mPtr);
186   delete arr;
187 
188   aValue.mU.mPtr = nullptr;
189   aValue.mType = nsSMILNullType::Singleton();
190 }
191 
192 nsresult
Assign(nsSMILValue & aDest,const nsSMILValue & aSrc) const193 SVGMotionSMILType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const
194 {
195   MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types");
196   MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL type");
197 
198   const MotionSegmentArray& srcArr = ExtractMotionSegmentArray(aSrc);
199   MotionSegmentArray& dstArr = ExtractMotionSegmentArray(aDest);
200   if (!dstArr.Assign(srcArr, fallible)) {
201     return NS_ERROR_OUT_OF_MEMORY;
202   }
203 
204   return NS_OK;
205 }
206 
207 bool
IsEqual(const nsSMILValue & aLeft,const nsSMILValue & aRight) const208 SVGMotionSMILType::IsEqual(const nsSMILValue& aLeft,
209                            const nsSMILValue& aRight) const
210 {
211   MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types");
212   MOZ_ASSERT(aLeft.mType == this, "Unexpected SMIL type");
213 
214   const MotionSegmentArray& leftArr = ExtractMotionSegmentArray(aLeft);
215   const MotionSegmentArray& rightArr = ExtractMotionSegmentArray(aRight);
216 
217   // If array-lengths don't match, we're trivially non-equal.
218   if (leftArr.Length() != rightArr.Length()) {
219     return false;
220   }
221 
222   // Array-lengths match -- check each array-entry for equality.
223   uint32_t length = leftArr.Length(); // == rightArr->Length(), if we get here
224   for (uint32_t i = 0; i < length; ++i) {
225     if (leftArr[i] != rightArr[i]) {
226       return false;
227     }
228   }
229 
230   return true; // If we get here, we found no differences.
231 }
232 
233 // Helper method for Add & CreateMatrix
234 inline static void
GetAngleAndPointAtDistance(Path * aPath,float aDistance,RotateType aRotateType,float & aRotateAngle,Point & aPoint)235 GetAngleAndPointAtDistance(Path* aPath, float aDistance,
236                            RotateType aRotateType,
237                            float& aRotateAngle, // in & out-param.
238                            Point& aPoint)       // out-param.
239 {
240   if (aRotateType == eRotateType_Explicit) {
241     // Leave aRotateAngle as-is.
242     aPoint = aPath->ComputePointAtLength(aDistance);
243   } else {
244     Point tangent; // Unit vector tangent to the point we find.
245     aPoint = aPath->ComputePointAtLength(aDistance, &tangent);
246     float tangentAngle = atan2(tangent.y, tangent.x);
247     if (aRotateType == eRotateType_Auto) {
248       aRotateAngle = tangentAngle;
249     } else {
250       MOZ_ASSERT(aRotateType == eRotateType_AutoReverse);
251       aRotateAngle = M_PI + tangentAngle;
252     }
253   }
254 }
255 
256 nsresult
Add(nsSMILValue & aDest,const nsSMILValue & aValueToAdd,uint32_t aCount) const257 SVGMotionSMILType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd,
258                        uint32_t aCount) const
259 {
260   MOZ_ASSERT(aDest.mType == aValueToAdd.mType,
261              "Incompatible SMIL types");
262   MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL type");
263 
264   MotionSegmentArray& dstArr = ExtractMotionSegmentArray(aDest);
265   const MotionSegmentArray& srcArr = ExtractMotionSegmentArray(aValueToAdd);
266 
267   // We're doing a simple add here (as opposed to a sandwich add below).  We
268   // only do this when we're accumulating a repeat result.
269   // NOTE: In other nsISMILTypes, we use this method with a barely-initialized
270   // |aDest| value to assist with "by" animation.  (In this case,
271   // "barely-initialized" would mean dstArr.Length() would be empty.)  However,
272   // we don't do this for <animateMotion>, because we instead use our "by"
273   // value to construct an equivalent "path" attribute, and we use *that* for
274   // our actual animation.
275   MOZ_ASSERT(srcArr.Length() == 1, "Invalid source segment arr to add");
276   MOZ_ASSERT(dstArr.Length() == 1, "Invalid dest segment arr to add to");
277   const MotionSegment& srcSeg = srcArr[0];
278   const MotionSegment& dstSeg = dstArr[0];
279   MOZ_ASSERT(srcSeg.mSegmentType == eSegmentType_PathPoint,
280              "expecting to be adding points from a motion path");
281   MOZ_ASSERT(dstSeg.mSegmentType == eSegmentType_PathPoint,
282              "expecting to be adding points from a motion path");
283 
284   const PathPointParams& srcParams = srcSeg.mU.mPathPointParams;
285   const PathPointParams& dstParams = dstSeg.mU.mPathPointParams;
286 
287   MOZ_ASSERT(srcSeg.mRotateType  == dstSeg.mRotateType &&
288              srcSeg.mRotateAngle == dstSeg.mRotateAngle,
289              "unexpected angle mismatch");
290   MOZ_ASSERT(srcParams.mPath == dstParams.mPath,
291              "unexpected path mismatch");
292   Path* path = srcParams.mPath;
293 
294   // Use destination to get our rotate angle.
295   float rotateAngle = dstSeg.mRotateAngle;
296   Point dstPt;
297   GetAngleAndPointAtDistance(path, dstParams.mDistToPoint, dstSeg.mRotateType,
298                              rotateAngle, dstPt);
299 
300   Point srcPt = path->ComputePointAtLength(srcParams.mDistToPoint);
301 
302   float newX = dstPt.x + srcPt.x * aCount;
303   float newY = dstPt.y + srcPt.y * aCount;
304 
305   // Replace destination's current value -- a point-on-a-path -- with the
306   // translation that results from our addition.
307   dstArr.ReplaceElementAt(0, MotionSegment(newX, newY, rotateAngle));
308   return NS_OK;
309 }
310 
311 nsresult
SandwichAdd(nsSMILValue & aDest,const nsSMILValue & aValueToAdd) const312 SVGMotionSMILType::SandwichAdd(nsSMILValue& aDest,
313                                const nsSMILValue& aValueToAdd) const
314 {
315   MOZ_ASSERT(aDest.mType == aValueToAdd.mType,
316              "Incompatible SMIL types");
317   MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL type");
318   MotionSegmentArray& dstArr = ExtractMotionSegmentArray(aDest);
319   const MotionSegmentArray& srcArr = ExtractMotionSegmentArray(aValueToAdd);
320 
321   // We're only expecting to be adding 1 segment on to the list
322   MOZ_ASSERT(srcArr.Length() == 1,
323              "Trying to do sandwich add of more than one value");
324 
325   if (!dstArr.AppendElement(srcArr[0], fallible)) {
326     return NS_ERROR_OUT_OF_MEMORY;
327   }
328 
329   return NS_OK;
330 }
331 
332 nsresult
ComputeDistance(const nsSMILValue & aFrom,const nsSMILValue & aTo,double & aDistance) const333 SVGMotionSMILType::ComputeDistance(const nsSMILValue& aFrom,
334                                    const nsSMILValue& aTo,
335                                    double& aDistance) const
336 {
337   MOZ_ASSERT(aFrom.mType == aTo.mType, "Incompatible SMIL types");
338   MOZ_ASSERT(aFrom.mType == this, "Unexpected SMIL type");
339   const MotionSegmentArray& fromArr = ExtractMotionSegmentArray(aFrom);
340   const MotionSegmentArray& toArr = ExtractMotionSegmentArray(aTo);
341 
342   // ComputeDistance is only used for calculating distances between single
343   // values in a values array. So we should only have one entry in each array.
344   MOZ_ASSERT(fromArr.Length() == 1,
345              "Wrong number of elements in from value");
346   MOZ_ASSERT(toArr.Length() == 1,
347              "Wrong number of elements in to value");
348 
349   const MotionSegment& from = fromArr[0];
350   const MotionSegment& to = toArr[0];
351 
352   MOZ_ASSERT(from.mSegmentType == to.mSegmentType,
353              "Mismatched MotionSegment types");
354   if (from.mSegmentType == eSegmentType_PathPoint) {
355     const PathPointParams& fromParams = from.mU.mPathPointParams;
356     const PathPointParams& toParams   = to.mU.mPathPointParams;
357     MOZ_ASSERT(fromParams.mPath == toParams.mPath,
358                "Interpolation endpoints should be from same path");
359     MOZ_ASSERT(fromParams.mDistToPoint <= toParams.mDistToPoint,
360                "To value shouldn't be before from value on path");
361     aDistance = fabs(toParams.mDistToPoint - fromParams.mDistToPoint);
362   } else {
363     const TranslationParams& fromParams = from.mU.mTranslationParams;
364     const TranslationParams& toParams   = to.mU.mTranslationParams;
365     float dX = toParams.mX - fromParams.mX;
366     float dY = toParams.mY - fromParams.mY;
367     aDistance = NS_hypot(dX, dY);
368   }
369 
370   return NS_OK;
371 }
372 
373 // Helper method for Interpolate()
374 static inline float
InterpolateFloat(const float & aStartFlt,const float & aEndFlt,const double & aUnitDistance)375 InterpolateFloat(const float& aStartFlt, const float& aEndFlt,
376                  const double& aUnitDistance)
377 {
378   return aStartFlt + aUnitDistance * (aEndFlt - aStartFlt);
379 }
380 
381 nsresult
Interpolate(const nsSMILValue & aStartVal,const nsSMILValue & aEndVal,double aUnitDistance,nsSMILValue & aResult) const382 SVGMotionSMILType::Interpolate(const nsSMILValue& aStartVal,
383                                const nsSMILValue& aEndVal,
384                                double aUnitDistance,
385                                nsSMILValue& aResult) const
386 {
387   MOZ_ASSERT(aStartVal.mType == aEndVal.mType,
388              "Trying to interpolate different types");
389   MOZ_ASSERT(aStartVal.mType == this,
390              "Unexpected types for interpolation");
391   MOZ_ASSERT(aResult.mType == this, "Unexpected result type");
392   MOZ_ASSERT(aUnitDistance >= 0.0 && aUnitDistance <= 1.0,
393              "unit distance value out of bounds");
394 
395   const MotionSegmentArray& startArr = ExtractMotionSegmentArray(aStartVal);
396   const MotionSegmentArray& endArr = ExtractMotionSegmentArray(aEndVal);
397   MotionSegmentArray& resultArr = ExtractMotionSegmentArray(aResult);
398 
399   MOZ_ASSERT(startArr.Length() <= 1,
400              "Invalid start-point for animateMotion interpolation");
401   MOZ_ASSERT(endArr.Length() == 1,
402              "Invalid end-point for animateMotion interpolation");
403   MOZ_ASSERT(resultArr.IsEmpty(),
404              "Expecting result to be just-initialized w/ empty array");
405 
406   const MotionSegment& endSeg = endArr[0];
407   MOZ_ASSERT(endSeg.mSegmentType == eSegmentType_PathPoint,
408              "Expecting to be interpolating along a path");
409 
410   const PathPointParams& endParams = endSeg.mU.mPathPointParams;
411   // NOTE: path & angle should match between start & end (since presumably
412   // start & end came from the same <animateMotion> element), unless start is
413   // empty. (as it would be for pure 'to' animation)
414   Path* path = endParams.mPath;
415   RotateType rotateType  = endSeg.mRotateType;
416   float rotateAngle      = endSeg.mRotateAngle;
417 
418   float startDist;
419   if (startArr.IsEmpty()) {
420     startDist = 0.0f;
421   } else {
422     const MotionSegment& startSeg = startArr[0];
423     MOZ_ASSERT(startSeg.mSegmentType == eSegmentType_PathPoint,
424                "Expecting to be interpolating along a path");
425     const PathPointParams& startParams = startSeg.mU.mPathPointParams;
426     MOZ_ASSERT(startSeg.mRotateType  == endSeg.mRotateType &&
427                startSeg.mRotateAngle == endSeg.mRotateAngle,
428                "unexpected angle mismatch");
429     MOZ_ASSERT(startParams.mPath == endParams.mPath,
430                "unexpected path mismatch");
431     startDist = startParams.mDistToPoint;
432   }
433 
434   // Get the interpolated distance along our path.
435   float resultDist = InterpolateFloat(startDist, endParams.mDistToPoint,
436                                       aUnitDistance);
437 
438   // Construct the intermediate result segment, and put it in our outparam.
439   // AppendElement has guaranteed success here, since Init() allocates 1 slot.
440   MOZ_ALWAYS_TRUE(resultArr.AppendElement(MotionSegment(path, resultDist,
441                                                         rotateType,
442                                                         rotateAngle),
443                                           fallible));
444   return NS_OK;
445 }
446 
447 /* static */ gfx::Matrix
CreateMatrix(const nsSMILValue & aSMILVal)448 SVGMotionSMILType::CreateMatrix(const nsSMILValue& aSMILVal)
449 {
450   const MotionSegmentArray& arr = ExtractMotionSegmentArray(aSMILVal);
451 
452   gfx::Matrix matrix;
453   uint32_t length = arr.Length();
454   for (uint32_t i = 0; i < length; i++) {
455     Point point;  // initialized below
456     float rotateAngle = arr[i].mRotateAngle; // might get updated below
457     if (arr[i].mSegmentType == eSegmentType_Translation) {
458       point.x = arr[i].mU.mTranslationParams.mX;
459       point.y = arr[i].mU.mTranslationParams.mY;
460       MOZ_ASSERT(arr[i].mRotateType == eRotateType_Explicit,
461                  "'auto'/'auto-reverse' should have been converted to "
462                  "explicit angles when we generated this translation");
463     } else {
464       GetAngleAndPointAtDistance(arr[i].mU.mPathPointParams.mPath,
465                                  arr[i].mU.mPathPointParams.mDistToPoint,
466                                  arr[i].mRotateType,
467                                  rotateAngle, point);
468     }
469     matrix.PreTranslate(point.x, point.y);
470     matrix.PreRotate(rotateAngle);
471   }
472   return matrix;
473 }
474 
475 /* static */ nsSMILValue
ConstructSMILValue(Path * aPath,float aDist,RotateType aRotateType,float aRotateAngle)476 SVGMotionSMILType::ConstructSMILValue(Path* aPath,
477                                       float aDist,
478                                       RotateType aRotateType,
479                                       float aRotateAngle)
480 {
481   nsSMILValue smilVal(&SVGMotionSMILType::sSingleton);
482   MotionSegmentArray& arr = ExtractMotionSegmentArray(smilVal);
483 
484   // AppendElement has guaranteed success here, since Init() allocates 1 slot.
485   MOZ_ALWAYS_TRUE(arr.AppendElement(MotionSegment(aPath, aDist,
486                                                   aRotateType, aRotateAngle),
487                                     fallible));
488   return smilVal;
489 }
490 
491 } // namespace mozilla
492