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