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