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