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