Merge mozilla-central to autoland on a CLOSED TREE
[gecko.git] / dom / svg / SVGMotionSMILType.cpp
blob15521ee54dba96d80fffa660c53609b88af2e4a2
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"
14 #include "nsDebug.h"
15 #include "nsMathUtils.h"
16 #include "nsISupportsUtils.h"
17 #include "nsTArray.h"
18 #include <math.h>
20 using namespace mozilla::gfx;
22 namespace mozilla {
24 /*static*/
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
33 float mX;
34 float mY;
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.
44 /**
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)
62 MotionSegment()
63 : mRotateType(eRotateType_Auto),
64 mRotateAngle(0.0),
65 mSegmentType(eSegmentType_Translation),
66 mU{} {}
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,
79 float aRotateAngle)
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)
103 ~MotionSegment() {
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.
116 return false;
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);
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)
142 union { // Union to let us hold the params for either segment-type.
143 TranslationParams mTranslationParams;
144 PathPointParams mPathPointParams;
145 } mU;
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");
166 aValue.mType = this;
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);
174 delete arr;
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;
191 return NS_OK;
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()) {
204 return false;
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;
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);
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;
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;
276 Point dstPt;
277 GetAngleAndPointAtDistance(path, dstParams.mDistToPoint, dstSeg.mRotateType,
278 rotateAngle, dstPt);
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));
288 return NS_OK;
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;
306 return NS_OK;
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);
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);
341 return NS_OK;
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;
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;
409 // Get the interpolated distance along our path.
410 float resultDist =
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));
417 return NS_OK;
420 /* static */ gfx::Matrix SVGMotionSMILType::CreateMatrix(
421 const SMILValue& aSMILVal) {
422 const MotionSegmentArray& arr = ExtractMotionSegmentArray(aSMILVal);
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);
440 matrix.PreTranslate(point.x, point.y);
441 matrix.PreRotate(rotateAngle);
443 return matrix;
446 /* static */
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));
456 return smilVal;
459 } // namespace mozilla