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/. */
7 /* implementation of nsISMILType for use by <animateMotion> element */
9 #include "SVGMotionSMILType.h"
11 #include "mozilla/SMILValue.h"
12 #include "mozilla/gfx/Point.h"
13 #include "gfx2DGlue.h"
15 #include "nsMathUtils.h"
16 #include "nsISupportsUtils.h"
20 using namespace mozilla::gfx
;
25 SVGMotionSMILType
SVGMotionSMILType::sSingleton
;
27 // Helper enum, for distinguishing between types of MotionSegment structs
28 enum SegmentType
{ eSegmentType_Translation
, eSegmentType_PathPoint
};
30 // Helper Structs: containers for params to define our MotionSegment
31 // (either simple translation or point-on-a-path)
32 struct TranslationParams
{ // Simple translation
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.
45 * Helper Struct: MotionSegment
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.)
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.
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)
63 : mRotateType(eRotateType_Auto
),
65 mSegmentType(eSegmentType_Translation
),
68 // Constructor for a translation
69 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
;
77 // Constructor for a point on a path (NOTE: AddRef's)
78 MotionSegment(Path
* aPath
, float aDistToPoint
, RotateType aRotateType
,
80 : mRotateType(aRotateType
),
81 mRotateAngle(aRotateAngle
),
82 mSegmentType(eSegmentType_PathPoint
) {
83 mU
.mPathPointParams
.mPath
= aPath
;
84 mU
.mPathPointParams
.mDistToPoint
= aDistToPoint
;
86 NS_ADDREF(mU
.mPathPointParams
.mPath
); // Retain a reference to path
89 // Copy constructor (NOTE: AddRef's if we're eSegmentType_PathPoint)
90 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
102 // Destructor (releases any reference we were holding onto)
104 if (mSegmentType
== eSegmentType_PathPoint
) {
105 NS_RELEASE(mU
.mPathPointParams
.mPath
);
109 // Comparison operators
110 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.
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
;
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
);
131 bool operator!=(const MotionSegment
& aOther
) const {
132 return !(*this == aOther
);
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)
142 union { // Union to let us hold the params for either segment-type.
143 TranslationParams mTranslationParams
;
144 PathPointParams mPathPointParams
;
148 using MotionSegmentArray
= FallibleTArray
<MotionSegment
>;
150 // Helper methods to cast SMILValue.mU.mPtr to the right pointer-type
151 static MotionSegmentArray
& ExtractMotionSegmentArray(SMILValue
& aValue
) {
152 return *static_cast<MotionSegmentArray
*>(aValue
.mU
.mPtr
);
155 static const MotionSegmentArray
& ExtractMotionSegmentArray(
156 const SMILValue
& aValue
) {
157 return *static_cast<const MotionSegmentArray
*>(aValue
.mU
.mPtr
);
160 // nsISMILType Methods
161 // -------------------
163 void SVGMotionSMILType::Init(SMILValue
& aValue
) const {
164 MOZ_ASSERT(aValue
.IsNull(), "Unexpected SMIL type");
167 aValue
.mU
.mPtr
= new MotionSegmentArray(1);
170 void SVGMotionSMILType::Destroy(SMILValue
& aValue
) const {
171 MOZ_ASSERT(aValue
.mType
== this, "Unexpected SMIL type");
173 MotionSegmentArray
* arr
= static_cast<MotionSegmentArray
*>(aValue
.mU
.mPtr
);
176 aValue
.mU
.mPtr
= nullptr;
177 aValue
.mType
= SMILNullType::Singleton();
180 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");
185 const MotionSegmentArray
& srcArr
= ExtractMotionSegmentArray(aSrc
);
186 MotionSegmentArray
& dstArr
= ExtractMotionSegmentArray(aDest
);
187 if (!dstArr
.Assign(srcArr
, fallible
)) {
188 return NS_ERROR_OUT_OF_MEMORY
;
194 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");
199 const MotionSegmentArray
& leftArr
= ExtractMotionSegmentArray(aLeft
);
200 const MotionSegmentArray
& rightArr
= ExtractMotionSegmentArray(aRight
);
202 // If array-lengths don't match, we're trivially non-equal.
203 if (leftArr
.Length() != rightArr
.Length()) {
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
]) {
215 return true; // If we get here, we found no differences.
218 // Helper method for Add & CreateMatrix
219 inline static void GetAngleAndPointAtDistance(
220 Path
* aPath
, float aDistance
, RotateType aRotateType
,
221 float& aRotateAngle
, // in & out-param.
222 Point
& aPoint
) // out-param.
224 if (aRotateType
== eRotateType_Explicit
) {
225 // Leave aRotateAngle as-is.
226 aPoint
= aPath
->ComputePointAtLength(aDistance
);
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
;
234 MOZ_ASSERT(aRotateType
== eRotateType_AutoReverse
);
235 aRotateAngle
= M_PI
+ tangentAngle
;
240 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");
245 MotionSegmentArray
& dstArr
= ExtractMotionSegmentArray(aDest
);
246 const MotionSegmentArray
& srcArr
= ExtractMotionSegmentArray(aValueToAdd
);
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");
265 const PathPointParams
& srcParams
= srcSeg
.mU
.mPathPointParams
;
266 const PathPointParams
& dstParams
= dstSeg
.mU
.mPathPointParams
;
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
;
274 // Use destination to get our rotate angle.
275 float rotateAngle
= dstSeg
.mRotateAngle
;
277 GetAngleAndPointAtDistance(path
, dstParams
.mDistToPoint
, dstSeg
.mRotateType
,
280 Point srcPt
= path
->ComputePointAtLength(srcParams
.mDistToPoint
);
282 float newX
= dstPt
.x
+ srcPt
.x
* aCount
;
283 float newY
= dstPt
.y
+ srcPt
.y
* aCount
;
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
));
291 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
);
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");
302 if (!dstArr
.AppendElement(srcArr
[0], fallible
)) {
303 return NS_ERROR_OUT_OF_MEMORY
;
309 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
);
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");
322 const MotionSegment
& from
= fromArr
[0];
323 const MotionSegment
& to
= toArr
[0];
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
);
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
);
344 // Helper method for Interpolate()
345 static inline float InterpolateFloat(const float& aStartFlt
,
346 const float& aEndFlt
,
347 const double& aUnitDistance
) {
348 return aStartFlt
+ aUnitDistance
* (aEndFlt
- aStartFlt
);
351 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");
362 const MotionSegmentArray
& startArr
= ExtractMotionSegmentArray(aStartVal
);
363 const MotionSegmentArray
& endArr
= ExtractMotionSegmentArray(aEndVal
);
364 MotionSegmentArray
& resultArr
= ExtractMotionSegmentArray(aResult
);
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");
371 const MotionSegment
& endSeg
= endArr
[0];
372 MOZ_ASSERT(endSeg
.mSegmentType
== eSegmentType_PathPoint
,
373 "Expecting to be interpolating along a path");
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
;
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.
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
;
409 // Get the interpolated distance along our path.
411 InterpolateFloat(startDist
, endParams
.mDistToPoint
, aUnitDistance
);
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
));
420 /* static */ gfx::Matrix
SVGMotionSMILType::CreateMatrix(
421 const SMILValue
& aSMILVal
) {
422 const MotionSegmentArray
& arr
= ExtractMotionSegmentArray(aSMILVal
);
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");
436 GetAngleAndPointAtDistance(arr
[i
].mU
.mPathPointParams
.mPath
,
437 arr
[i
].mU
.mPathPointParams
.mDistToPoint
,
438 arr
[i
].mRotateType
, rotateAngle
, point
);
440 matrix
.PreTranslate(point
.x
, point
.y
);
441 matrix
.PreRotate(rotateAngle
);
447 SMILValue
SVGMotionSMILType::ConstructSMILValue(Path
* aPath
, float aDist
,
448 RotateType aRotateType
,
449 float aRotateAngle
) {
450 SMILValue
smilVal(&SVGMotionSMILType::sSingleton
);
451 MotionSegmentArray
& arr
= ExtractMotionSegmentArray(smilVal
);
453 // AppendElement has guaranteed success here, since Init() allocates 1 slot.
454 MOZ_ALWAYS_TRUE(arr
.AppendElement(
455 MotionSegment(aPath
, aDist
, aRotateType
, aRotateAngle
), fallible
));
459 } // namespace mozilla