SVGMotionSMILAnimationFunction.cpp (14592B)
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 #include "SVGMotionSMILAnimationFunction.h" 8 9 #include "SVGAnimatedOrient.h" 10 #include "SVGMotionSMILPathUtils.h" 11 #include "SVGMotionSMILType.h" 12 #include "mozilla/SMILParserUtils.h" 13 #include "mozilla/dom/SVGAnimationElement.h" 14 #include "mozilla/dom/SVGMPathElement.h" 15 #include "mozilla/dom/SVGPathElement.h" 16 #include "mozilla/gfx/2D.h" 17 #include "nsAttrValue.h" 18 #include "nsAttrValueInlines.h" 19 #include "nsAttrValueOrString.h" 20 21 using namespace mozilla::dom; 22 using namespace mozilla::dom::SVGAngle_Binding; 23 using namespace mozilla::gfx; 24 25 namespace mozilla { 26 27 SVGMotionSMILAnimationFunction::SVGMotionSMILAnimationFunction() 28 : mRotateType(eRotateType_Explicit), 29 mRotateAngle(0.0f), 30 mPathSourceType(ePathSourceType_None), 31 mIsPathStale(true) // Try to initialize path on first GetValues call 32 {} 33 34 void SVGMotionSMILAnimationFunction::MarkStaleIfAttributeAffectsPath( 35 nsAtom* aAttribute) { 36 bool isAffected; 37 if (aAttribute == nsGkAtoms::path) { 38 isAffected = (mPathSourceType <= ePathSourceType_PathAttr); 39 } else if (aAttribute == nsGkAtoms::values) { 40 isAffected = (mPathSourceType <= ePathSourceType_ValuesAttr); 41 } else if (aAttribute == nsGkAtoms::from || aAttribute == nsGkAtoms::to) { 42 isAffected = (mPathSourceType <= ePathSourceType_ToAttr); 43 } else if (aAttribute == nsGkAtoms::by) { 44 isAffected = (mPathSourceType <= ePathSourceType_ByAttr); 45 } else { 46 MOZ_ASSERT_UNREACHABLE( 47 "Should only call this method for path-describing " 48 "attrs"); 49 isAffected = false; 50 } 51 52 if (isAffected) { 53 mIsPathStale = true; 54 mHasChanged = true; 55 } 56 } 57 58 bool SVGMotionSMILAnimationFunction::SetAttr(nsAtom* aAttribute, 59 const nsAString& aValue, 60 nsAttrValue& aResult, 61 nsresult* aParseResult) { 62 // Handle motion-specific attrs 63 if (aAttribute == nsGkAtoms::keyPoints) { 64 nsresult rv = SetKeyPoints(aValue, aResult); 65 if (aParseResult) { 66 *aParseResult = rv; 67 } 68 } else if (aAttribute == nsGkAtoms::rotate) { 69 nsresult rv = SetRotate(aValue, aResult); 70 if (aParseResult) { 71 *aParseResult = rv; 72 } 73 } else if (aAttribute == nsGkAtoms::path || aAttribute == nsGkAtoms::by || 74 aAttribute == nsGkAtoms::from || aAttribute == nsGkAtoms::to || 75 aAttribute == nsGkAtoms::values) { 76 aResult.SetTo(aValue); 77 MarkStaleIfAttributeAffectsPath(aAttribute); 78 if (aParseResult) { 79 *aParseResult = NS_OK; 80 } 81 } else { 82 // Defer to superclass method 83 return SMILAnimationFunction::SetAttr(aAttribute, aValue, aResult, 84 aParseResult); 85 } 86 87 return true; 88 } 89 90 bool SVGMotionSMILAnimationFunction::UnsetAttr(nsAtom* aAttribute) { 91 if (aAttribute == nsGkAtoms::keyPoints) { 92 UnsetKeyPoints(); 93 } else if (aAttribute == nsGkAtoms::rotate) { 94 UnsetRotate(); 95 } else if (aAttribute == nsGkAtoms::path || aAttribute == nsGkAtoms::by || 96 aAttribute == nsGkAtoms::from || aAttribute == nsGkAtoms::to || 97 aAttribute == nsGkAtoms::values) { 98 MarkStaleIfAttributeAffectsPath(aAttribute); 99 } else { 100 // Defer to superclass method 101 return SMILAnimationFunction::UnsetAttr(aAttribute); 102 } 103 104 return true; 105 } 106 107 SMILAnimationFunction::SMILCalcMode 108 SVGMotionSMILAnimationFunction::GetCalcMode() const { 109 const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode); 110 if (!value) { 111 return CALC_PACED; // animateMotion defaults to calcMode="paced" 112 } 113 114 return SMILCalcMode(value->GetEnumValue()); 115 } 116 117 //---------------------------------------------------------------------- 118 // Helpers for GetValues 119 120 /* 121 * Returns the first <mpath> child of the given element 122 */ 123 124 static SVGMPathElement* GetFirstMPathChild(nsIContent* aElem) { 125 for (nsIContent* child = aElem->GetFirstChild(); child; 126 child = child->GetNextSibling()) { 127 if (child->IsSVGElement(nsGkAtoms::mpath)) { 128 return static_cast<SVGMPathElement*>(child); 129 } 130 } 131 132 return nullptr; 133 } 134 135 void SVGMotionSMILAnimationFunction::RebuildPathAndVerticesFromBasicAttrs( 136 const nsIContent* aContextElem) { 137 MOZ_ASSERT(!HasAttr(nsGkAtoms::path), 138 "Should be using |path| attr if we have it"); 139 MOZ_ASSERT(!mPath, "regenerating when we already have path"); 140 MOZ_ASSERT(mPathVertices.IsEmpty(), 141 "regenerating when we already have vertices"); 142 143 const auto* context = SVGElement::FromNode(aContextElem); 144 if (!context) { 145 NS_ERROR("Uh oh, SVG animateMotion element targeting a non-SVG node"); 146 return; 147 } 148 SVGMotionSMILPathUtils::PathGenerator pathGenerator(context); 149 150 bool success = false; 151 if (HasAttr(nsGkAtoms::values)) { 152 // Generate path based on our values array 153 mPathSourceType = ePathSourceType_ValuesAttr; 154 nsAttrValueOrString valuesVal(GetAttr(nsGkAtoms::values)); 155 SVGMotionSMILPathUtils::MotionValueParser parser(&pathGenerator, 156 &mPathVertices); 157 success = SMILParserUtils::ParseValuesGeneric(valuesVal.String(), parser); 158 } else if (HasAttr(nsGkAtoms::to) || HasAttr(nsGkAtoms::by)) { 159 // Apply 'from' value (or a dummy 0,0 'from' value) 160 if (HasAttr(nsGkAtoms::from)) { 161 nsAttrValueOrString fromVal(GetAttr(nsGkAtoms::from)); 162 success = pathGenerator.MoveToAbsolute(fromVal.String()); 163 if (!mPathVertices.AppendElement(0.0, fallible)) { 164 success = false; 165 } 166 } else { 167 // Create dummy 'from' value at 0,0, if we're doing by-animation. 168 // (NOTE: We don't add the dummy 0-point to our list for *to-animation*, 169 // because the SMILAnimationFunction logic for to-animation doesn't 170 // expect a dummy value. It only expects one value: the final 'to' value.) 171 pathGenerator.MoveToOrigin(); 172 success = true; 173 if (!HasAttr(nsGkAtoms::to)) { 174 if (!mPathVertices.AppendElement(0.0, fallible)) { 175 success = false; 176 } 177 } 178 } 179 180 // Apply 'to' or 'by' value 181 if (success) { 182 double dist; 183 if (HasAttr(nsGkAtoms::to)) { 184 mPathSourceType = ePathSourceType_ToAttr; 185 nsAttrValueOrString toVal(GetAttr(nsGkAtoms::to)); 186 success = pathGenerator.LineToAbsolute(toVal.String(), dist); 187 } else { // HasAttr(nsGkAtoms::by) 188 mPathSourceType = ePathSourceType_ByAttr; 189 nsAttrValueOrString byVal(GetAttr(nsGkAtoms::by)); 190 success = pathGenerator.LineToRelative(byVal.String(), dist); 191 } 192 if (success) { 193 if (!mPathVertices.AppendElement(dist, fallible)) { 194 success = false; 195 } 196 } 197 } 198 } 199 if (success) { 200 mPath = pathGenerator.GetResultingPath(); 201 } else { 202 // Parse failure. Leave path as null, and clear path-related member data. 203 mPathVertices.Clear(); 204 } 205 } 206 207 void SVGMotionSMILAnimationFunction::RebuildPathAndVerticesFromMpathElem( 208 SVGMPathElement* aMpathElem) { 209 mPathSourceType = ePathSourceType_Mpath; 210 211 // Use the shape that's the target of our chosen <mpath> child. 212 SVGGeometryElement* shape = aMpathElem->GetReferencedPath(); 213 if (!shape || !shape->HasValidDimensions()) { 214 return; 215 } 216 if (!shape->GetDistancesFromOriginToEndsOfVisibleSegments(&mPathVertices)) { 217 mPathVertices.Clear(); 218 return; 219 } 220 if (mPathVertices.IsEmpty()) { 221 return; 222 } 223 mPath = shape->GetOrBuildPathForMeasuring(); 224 if (!mPath) { 225 mPathVertices.Clear(); 226 return; 227 } 228 } 229 230 void SVGMotionSMILAnimationFunction::RebuildPathAndVerticesFromPathAttr() { 231 nsString pathSpec(nsAttrValueOrString(GetAttr(nsGkAtoms::path)).String()); 232 mPathSourceType = ePathSourceType_PathAttr; 233 234 // Generate Path from |path| attr 235 SVGPathData path{NS_ConvertUTF16toUTF8(pathSpec)}; 236 237 // We must explicitly check that the parse produces at least one path segment 238 // (if the path data doesn't begin with a valid "M", then it's invalid). 239 if (path.IsEmpty()) { 240 return; 241 } 242 243 mPath = path.BuildPathForMeasuring(1.0f); 244 bool ok = path.GetDistancesFromOriginToEndsOfVisibleSegments(&mPathVertices); 245 if (!ok || mPathVertices.IsEmpty() || !mPath) { 246 mPath = nullptr; 247 mPathVertices.Clear(); 248 } 249 } 250 251 // Helper to regenerate our path representation & its list of vertices 252 void SVGMotionSMILAnimationFunction::RebuildPathAndVertices( 253 const nsIContent* aTargetElement) { 254 MOZ_ASSERT(mIsPathStale, "rebuilding path when it isn't stale"); 255 256 // Clear stale data 257 mPath = nullptr; 258 mPathVertices.Clear(); 259 mPathSourceType = ePathSourceType_None; 260 261 // Do we have a mpath child? if so, it trumps everything. Otherwise, we look 262 // through our list of path-defining attributes, in order of priority. 263 SVGMPathElement* firstMpathChild = GetFirstMPathChild(mAnimationElement); 264 265 if (firstMpathChild) { 266 RebuildPathAndVerticesFromMpathElem(firstMpathChild); 267 mValueNeedsReparsingEverySample = false; 268 } else if (HasAttr(nsGkAtoms::path)) { 269 RebuildPathAndVerticesFromPathAttr(); 270 mValueNeedsReparsingEverySample = false; 271 } else { 272 // Get path & vertices from basic SMIL attrs: from/by/to/values 273 RebuildPathAndVerticesFromBasicAttrs(aTargetElement); 274 mValueNeedsReparsingEverySample = true; 275 } 276 mIsPathStale = false; 277 } 278 279 nsresult SVGMotionSMILAnimationFunction::GenerateValuesForPathAndPoints( 280 Path* aPath, bool aIsKeyPoints, FallibleTArray<double>& aPointDistances, 281 SMILValueArray& aResult) { 282 MOZ_ASSERT(aResult.IsEmpty(), "outparam is non-empty"); 283 284 // If we're using "keyPoints" as our list of input distances, then we need 285 // to de-normalize from the [0, 1] scale to the [0, totalPathLen] scale. 286 double distanceMultiplier = aIsKeyPoints ? aPath->ComputeLength() : 1.0; 287 if (!std::isfinite(distanceMultiplier)) { 288 return NS_ERROR_FAILURE; 289 } 290 const uint32_t numPoints = aPointDistances.Length(); 291 for (uint32_t i = 0; i < numPoints; ++i) { 292 double curDist = aPointDistances[i] * distanceMultiplier; 293 if (!std::isfinite(curDist)) { 294 return NS_ERROR_FAILURE; 295 } 296 if (!aResult.AppendElement(SVGMotionSMILType::ConstructSMILValue( 297 aPath, curDist, mRotateType, mRotateAngle), 298 fallible)) { 299 return NS_ERROR_OUT_OF_MEMORY; 300 } 301 } 302 return NS_OK; 303 } 304 305 nsresult SVGMotionSMILAnimationFunction::GetValues(const SMILAttr& aSMILAttr, 306 SMILValueArray& aResult) { 307 if (mIsPathStale) { 308 RebuildPathAndVertices(aSMILAttr.GetTargetNode()); 309 } 310 MOZ_ASSERT(!mIsPathStale, "Forgot to clear 'is path stale' state"); 311 312 if (!mPath) { 313 // This could be due to e.g. a parse error. 314 MOZ_ASSERT(mPathVertices.IsEmpty(), "have vertices but no path"); 315 return NS_ERROR_FAILURE; 316 } 317 MOZ_ASSERT(!mPathVertices.IsEmpty(), "have a path but no vertices"); 318 319 // Now: Make the actual list of SMILValues (using keyPoints, if set) 320 bool isUsingKeyPoints = !mKeyPoints.IsEmpty(); 321 return GenerateValuesForPathAndPoints( 322 mPath, isUsingKeyPoints, isUsingKeyPoints ? mKeyPoints : mPathVertices, 323 aResult); 324 } 325 326 void SVGMotionSMILAnimationFunction::CheckValueListDependentAttrs( 327 uint32_t aNumValues) { 328 // Call superclass method. 329 SMILAnimationFunction::CheckValueListDependentAttrs(aNumValues); 330 331 // Added behavior: Do checks specific to keyPoints. 332 CheckKeyPoints(); 333 } 334 335 bool SVGMotionSMILAnimationFunction::IsToAnimation() const { 336 // Rely on inherited method, but not if we have an <mpath> child or a |path| 337 // attribute, because they'll override any 'to' attr we might have. 338 // NOTE: We can't rely on mPathSourceType, because it might not have been 339 // set to a useful value yet (or it might be stale). 340 return !GetFirstMPathChild(mAnimationElement) && !HasAttr(nsGkAtoms::path) && 341 SMILAnimationFunction::IsToAnimation(); 342 } 343 344 void SVGMotionSMILAnimationFunction::CheckKeyPoints() { 345 if (!HasAttr(nsGkAtoms::keyPoints)) return; 346 347 // attribute is ignored for calcMode="paced" (even if it's got errors) 348 if (GetCalcMode() == CALC_PACED) { 349 SetKeyPointsErrorFlag(false); 350 return; 351 } 352 353 if (mKeyPoints.Length() != mKeyTimes.Length()) { 354 // there must be exactly as many keyPoints as keyTimes 355 SetKeyPointsErrorFlag(true); 356 return; 357 } 358 359 // Nothing else to check -- we can catch all keyPoints errors elsewhere. 360 // - Formatting & range issues will be caught in SetKeyPoints, and will 361 // result in an empty mKeyPoints array, which will drop us into the error 362 // case above. 363 SetKeyPointsErrorFlag(false); 364 } 365 366 nsresult SVGMotionSMILAnimationFunction::SetKeyPoints( 367 const nsAString& aKeyPoints, nsAttrValue& aResult) { 368 mKeyPoints.Clear(); 369 aResult.SetTo(aKeyPoints); 370 371 mHasChanged = true; 372 373 if (!SMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyPoints, false, 374 mKeyPoints)) { 375 mKeyPoints.Clear(); 376 return NS_ERROR_FAILURE; 377 } 378 379 return NS_OK; 380 } 381 382 void SVGMotionSMILAnimationFunction::UnsetKeyPoints() { 383 mKeyPoints.Clear(); 384 SetKeyPointsErrorFlag(false); 385 mHasChanged = true; 386 } 387 388 nsresult SVGMotionSMILAnimationFunction::SetRotate(const nsAString& aRotate, 389 nsAttrValue& aResult) { 390 mHasChanged = true; 391 392 aResult.SetTo(aRotate); 393 if (aRotate.EqualsLiteral("auto")) { 394 mRotateType = eRotateType_Auto; 395 } else if (aRotate.EqualsLiteral("auto-reverse")) { 396 mRotateType = eRotateType_AutoReverse; 397 } else { 398 mRotateType = eRotateType_Explicit; 399 400 uint16_t angleUnit; 401 if (!SVGAnimatedOrient::GetValueFromString(aRotate, mRotateAngle, 402 &angleUnit)) { 403 mRotateAngle = 0.0f; // set default rotate angle 404 // XXX report to console? 405 return NS_ERROR_DOM_SYNTAX_ERR; 406 } 407 408 // Convert to radian units, if we're not already in radians. 409 if (angleUnit != SVG_ANGLETYPE_RAD) { 410 mRotateAngle *= SVGAnimatedOrient::GetDegreesPerUnit(angleUnit) / 411 SVGAnimatedOrient::GetDegreesPerUnit(SVG_ANGLETYPE_RAD); 412 } 413 } 414 return NS_OK; 415 } 416 417 void SVGMotionSMILAnimationFunction::UnsetRotate() { 418 mRotateAngle = 0.0f; // default value 419 mRotateType = eRotateType_Explicit; 420 mHasChanged = true; 421 } 422 423 } // namespace mozilla