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 #include "SVGMotionSMILAnimationFunction.h"
9 #include "mozilla/dom/SVGAnimationElement.h"
10 #include "mozilla/dom/SVGPathElement.h"
11 #include "mozilla/dom/SVGMPathElement.h"
12 #include "mozilla/gfx/2D.h"
13 #include "mozilla/SMILParserUtils.h"
14 #include "nsAttrValue.h"
15 #include "nsAttrValueInlines.h"
16 #include "SVGAnimatedOrient.h"
17 #include "SVGMotionSMILPathUtils.h"
18 #include "SVGMotionSMILType.h"
19 #include "SVGPathDataParser.h"
21 using namespace mozilla::dom
;
22 using namespace mozilla::dom::SVGAngle_Binding
;
23 using namespace mozilla::gfx
;
27 SVGMotionSMILAnimationFunction::SVGMotionSMILAnimationFunction()
28 : mRotateType(eRotateType_Explicit
),
30 mPathSourceType(ePathSourceType_None
),
31 mIsPathStale(true) // Try to initialize path on first GetValues call
34 void SVGMotionSMILAnimationFunction::MarkStaleIfAttributeAffectsPath(
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
);
46 MOZ_ASSERT_UNREACHABLE(
47 "Should only call this method for path-describing "
58 bool SVGMotionSMILAnimationFunction::SetAttr(nsAtom
* aAttribute
,
59 const nsAString
& aValue
,
61 nsresult
* aParseResult
) {
62 // Handle motion-specific attrs
63 if (aAttribute
== nsGkAtoms::keyPoints
) {
64 nsresult rv
= SetKeyPoints(aValue
, aResult
);
68 } else if (aAttribute
== nsGkAtoms::rotate
) {
69 nsresult rv
= SetRotate(aValue
, aResult
);
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
);
79 *aParseResult
= NS_OK
;
82 // Defer to superclass method
83 return SMILAnimationFunction::SetAttr(aAttribute
, aValue
, aResult
,
90 bool SVGMotionSMILAnimationFunction::UnsetAttr(nsAtom
* aAttribute
) {
91 if (aAttribute
== nsGkAtoms::keyPoints
) {
93 } else if (aAttribute
== nsGkAtoms::rotate
) {
95 } else if (aAttribute
== nsGkAtoms::path
|| aAttribute
== nsGkAtoms::by
||
96 aAttribute
== nsGkAtoms::from
|| aAttribute
== nsGkAtoms::to
||
97 aAttribute
== nsGkAtoms::values
) {
98 MarkStaleIfAttributeAffectsPath(aAttribute
);
100 // Defer to superclass method
101 return SMILAnimationFunction::UnsetAttr(aAttribute
);
107 SMILAnimationFunction::SMILCalcMode
108 SVGMotionSMILAnimationFunction::GetCalcMode() const {
109 const nsAttrValue
* value
= GetAttr(nsGkAtoms::calcMode
);
111 return CALC_PACED
; // animateMotion defaults to calcMode="paced"
114 return SMILCalcMode(value
->GetEnumValue());
117 //----------------------------------------------------------------------
118 // Helpers for GetValues
121 * Returns the first <mpath> child of the given element
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
);
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");
143 const auto* context
= SVGElement::FromNode(aContextElem
);
145 NS_ERROR("Uh oh, SVG animateMotion element targeting a non-SVG node");
148 SVGMotionSMILPathUtils::PathGenerator
pathGenerator(context
);
150 bool success
= false;
151 if (HasAttr(nsGkAtoms::values
)) {
152 // Generate path based on our values array
153 mPathSourceType
= ePathSourceType_ValuesAttr
;
154 const nsAString
& valuesStr
= GetAttr(nsGkAtoms::values
)->GetStringValue();
155 SVGMotionSMILPathUtils::MotionValueParser
parser(&pathGenerator
,
157 success
= SMILParserUtils::ParseValuesGeneric(valuesStr
, 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 const nsAString
& fromStr
= GetAttr(nsGkAtoms::from
)->GetStringValue();
162 success
= pathGenerator
.MoveToAbsolute(fromStr
);
163 if (!mPathVertices
.AppendElement(0.0, fallible
)) {
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();
173 if (!HasAttr(nsGkAtoms::to
)) {
174 if (!mPathVertices
.AppendElement(0.0, fallible
)) {
180 // Apply 'to' or 'by' value
183 if (HasAttr(nsGkAtoms::to
)) {
184 mPathSourceType
= ePathSourceType_ToAttr
;
185 const nsAString
& toStr
= GetAttr(nsGkAtoms::to
)->GetStringValue();
186 success
= pathGenerator
.LineToAbsolute(toStr
, dist
);
187 } else { // HasAttr(nsGkAtoms::by)
188 mPathSourceType
= ePathSourceType_ByAttr
;
189 const nsAString
& byStr
= GetAttr(nsGkAtoms::by
)->GetStringValue();
190 success
= pathGenerator
.LineToRelative(byStr
, dist
);
193 if (!mPathVertices
.AppendElement(dist
, fallible
)) {
200 mPath
= pathGenerator
.GetResultingPath();
202 // Parse failure. Leave path as null, and clear path-related member data.
203 mPathVertices
.Clear();
207 void SVGMotionSMILAnimationFunction::RebuildPathAndVerticesFromMpathElem(
208 SVGMPathElement
* aMpathElem
) {
209 mPathSourceType
= ePathSourceType_Mpath
;
211 // Use the shape that's the target of our chosen <mpath> child.
212 SVGGeometryElement
* shapeElem
= aMpathElem
->GetReferencedPath();
213 if (shapeElem
&& shapeElem
->HasValidDimensions()) {
214 bool ok
= shapeElem
->GetDistancesFromOriginToEndsOfVisibleSegments(
216 if (ok
&& mPathVertices
.Length()) {
217 mPath
= shapeElem
->GetOrBuildPathForMeasuring();
222 void SVGMotionSMILAnimationFunction::RebuildPathAndVerticesFromPathAttr() {
223 const nsAString
& pathSpec
= GetAttr(nsGkAtoms::path
)->GetStringValue();
224 mPathSourceType
= ePathSourceType_PathAttr
;
226 // Generate Path from |path| attr
228 SVGPathDataParser
pathParser(pathSpec
, &path
);
230 // We ignore any failure returned from Parse() since the SVG spec says to
231 // accept all segments up to the first invalid token. Instead we must
232 // explicitly check that the parse produces at least one path segment (if
233 // the path data doesn't begin with a valid "M", then it's invalid).
235 if (!path
.Length()) {
239 mPath
= path
.BuildPathForMeasuring();
240 bool ok
= path
.GetDistancesFromOriginToEndsOfVisibleSegments(&mPathVertices
);
241 if (!ok
|| !mPathVertices
.Length()) {
243 mPathVertices
.Clear();
247 // Helper to regenerate our path representation & its list of vertices
248 void SVGMotionSMILAnimationFunction::RebuildPathAndVertices(
249 const nsIContent
* aTargetElement
) {
250 MOZ_ASSERT(mIsPathStale
, "rebuilding path when it isn't stale");
254 mPathVertices
.Clear();
255 mPathSourceType
= ePathSourceType_None
;
257 // Do we have a mpath child? if so, it trumps everything. Otherwise, we look
258 // through our list of path-defining attributes, in order of priority.
259 SVGMPathElement
* firstMpathChild
= GetFirstMPathChild(mAnimationElement
);
261 if (firstMpathChild
) {
262 RebuildPathAndVerticesFromMpathElem(firstMpathChild
);
263 mValueNeedsReparsingEverySample
= false;
264 } else if (HasAttr(nsGkAtoms::path
)) {
265 RebuildPathAndVerticesFromPathAttr();
266 mValueNeedsReparsingEverySample
= false;
268 // Get path & vertices from basic SMIL attrs: from/by/to/values
270 RebuildPathAndVerticesFromBasicAttrs(aTargetElement
);
271 mValueNeedsReparsingEverySample
= true;
273 mIsPathStale
= false;
276 bool SVGMotionSMILAnimationFunction::GenerateValuesForPathAndPoints(
277 Path
* aPath
, bool aIsKeyPoints
, FallibleTArray
<double>& aPointDistances
,
278 SMILValueArray
& aResult
) {
279 MOZ_ASSERT(aResult
.IsEmpty(), "outparam is non-empty");
281 // If we're using "keyPoints" as our list of input distances, then we need
282 // to de-normalize from the [0, 1] scale to the [0, totalPathLen] scale.
283 double distanceMultiplier
= aIsKeyPoints
? aPath
->ComputeLength() : 1.0;
284 const uint32_t numPoints
= aPointDistances
.Length();
285 for (uint32_t i
= 0; i
< numPoints
; ++i
) {
286 double curDist
= aPointDistances
[i
] * distanceMultiplier
;
287 if (!aResult
.AppendElement(SVGMotionSMILType::ConstructSMILValue(
288 aPath
, curDist
, mRotateType
, mRotateAngle
),
296 nsresult
SVGMotionSMILAnimationFunction::GetValues(const SMILAttr
& aSMILAttr
,
297 SMILValueArray
& aResult
) {
299 RebuildPathAndVertices(aSMILAttr
.GetTargetNode());
301 MOZ_ASSERT(!mIsPathStale
, "Forgot to clear 'is path stale' state");
304 // This could be due to e.g. a parse error.
305 MOZ_ASSERT(mPathVertices
.IsEmpty(), "have vertices but no path");
306 return NS_ERROR_FAILURE
;
308 MOZ_ASSERT(!mPathVertices
.IsEmpty(), "have a path but no vertices");
310 // Now: Make the actual list of SMILValues (using keyPoints, if set)
311 bool isUsingKeyPoints
= !mKeyPoints
.IsEmpty();
312 bool success
= GenerateValuesForPathAndPoints(
313 mPath
, isUsingKeyPoints
, isUsingKeyPoints
? mKeyPoints
: mPathVertices
,
316 return NS_ERROR_OUT_OF_MEMORY
;
322 void SVGMotionSMILAnimationFunction::CheckValueListDependentAttrs(
323 uint32_t aNumValues
) {
324 // Call superclass method.
325 SMILAnimationFunction::CheckValueListDependentAttrs(aNumValues
);
327 // Added behavior: Do checks specific to keyPoints.
331 bool SVGMotionSMILAnimationFunction::IsToAnimation() const {
332 // Rely on inherited method, but not if we have an <mpath> child or a |path|
333 // attribute, because they'll override any 'to' attr we might have.
334 // NOTE: We can't rely on mPathSourceType, because it might not have been
335 // set to a useful value yet (or it might be stale).
336 return !GetFirstMPathChild(mAnimationElement
) && !HasAttr(nsGkAtoms::path
) &&
337 SMILAnimationFunction::IsToAnimation();
340 void SVGMotionSMILAnimationFunction::CheckKeyPoints() {
341 if (!HasAttr(nsGkAtoms::keyPoints
)) return;
343 // attribute is ignored for calcMode="paced" (even if it's got errors)
344 if (GetCalcMode() == CALC_PACED
) {
345 SetKeyPointsErrorFlag(false);
348 if (mKeyPoints
.Length() != mKeyTimes
.Length()) {
349 // there must be exactly as many keyPoints as keyTimes
350 SetKeyPointsErrorFlag(true);
354 // Nothing else to check -- we can catch all keyPoints errors elsewhere.
355 // - Formatting & range issues will be caught in SetKeyPoints, and will
356 // result in an empty mKeyPoints array, which will drop us into the error
360 nsresult
SVGMotionSMILAnimationFunction::SetKeyPoints(
361 const nsAString
& aKeyPoints
, nsAttrValue
& aResult
) {
363 aResult
.SetTo(aKeyPoints
);
367 if (!SMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyPoints
, false,
370 return NS_ERROR_FAILURE
;
376 void SVGMotionSMILAnimationFunction::UnsetKeyPoints() {
378 SetKeyPointsErrorFlag(false);
382 nsresult
SVGMotionSMILAnimationFunction::SetRotate(const nsAString
& aRotate
,
383 nsAttrValue
& aResult
) {
386 aResult
.SetTo(aRotate
);
387 if (aRotate
.EqualsLiteral("auto")) {
388 mRotateType
= eRotateType_Auto
;
389 } else if (aRotate
.EqualsLiteral("auto-reverse")) {
390 mRotateType
= eRotateType_AutoReverse
;
392 mRotateType
= eRotateType_Explicit
;
395 if (!SVGAnimatedOrient::GetValueFromString(aRotate
, mRotateAngle
,
397 mRotateAngle
= 0.0f
; // set default rotate angle
398 // XXX report to console?
399 return NS_ERROR_DOM_SYNTAX_ERR
;
402 // Convert to radian units, if we're not already in radians.
403 if (angleUnit
!= SVG_ANGLETYPE_RAD
) {
404 mRotateAngle
*= SVGAnimatedOrient::GetDegreesPerUnit(angleUnit
) /
405 SVGAnimatedOrient::GetDegreesPerUnit(SVG_ANGLETYPE_RAD
);
411 void SVGMotionSMILAnimationFunction::UnsetRotate() {
412 mRotateAngle
= 0.0f
; // default value
413 mRotateType
= eRotateType_Explicit
;
417 } // namespace mozilla