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