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