Coverage Report

Created: 2018-09-25 14:53

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