gecko-dev/dom/svg/SVGMotionSMILAnimationFunction.cpp

465 lines
15 KiB
C++

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "SVGMotionSMILAnimationFunction.h"
#include "mozilla/dom/SVGAnimationElement.h"
#include "mozilla/dom/SVGPathElement.h" // for nsSVGPathList
#include "mozilla/dom/SVGMPathElement.h"
#include "mozilla/gfx/2D.h"
#include "nsAttrValue.h"
#include "nsAttrValueInlines.h"
#include "nsSMILParserUtils.h"
#include "nsSVGAngle.h"
#include "nsSVGPathDataParser.h"
#include "SVGMotionSMILType.h"
#include "SVGMotionSMILPathUtils.h"
using namespace mozilla::dom;
using namespace mozilla::gfx;
namespace mozilla {
SVGMotionSMILAnimationFunction::SVGMotionSMILAnimationFunction()
: mRotateType(eRotateType_Explicit),
mRotateAngle(0.0f),
mPathSourceType(ePathSourceType_None),
mIsPathStale(true) // Try to initialize path on first GetValues call
{
}
void
SVGMotionSMILAnimationFunction::MarkStaleIfAttributeAffectsPath(nsIAtom* aAttribute)
{
bool isAffected;
if (aAttribute == nsGkAtoms::path) {
isAffected = (mPathSourceType <= ePathSourceType_PathAttr);
} else if (aAttribute == nsGkAtoms::values) {
isAffected = (mPathSourceType <= ePathSourceType_ValuesAttr);
} else if (aAttribute == nsGkAtoms::from ||
aAttribute == nsGkAtoms::to) {
isAffected = (mPathSourceType <= ePathSourceType_ToAttr);
} else if (aAttribute == nsGkAtoms::by) {
isAffected = (mPathSourceType <= ePathSourceType_ByAttr);
} else {
NS_NOTREACHED("Should only call this method for path-describing attrs");
isAffected = false;
}
if (isAffected) {
mIsPathStale = true;
mHasChanged = true;
}
}
bool
SVGMotionSMILAnimationFunction::SetAttr(nsIAtom* aAttribute,
const nsAString& aValue,
nsAttrValue& aResult,
nsresult* aParseResult)
{
// Handle motion-specific attrs
if (aAttribute == nsGkAtoms::keyPoints) {
nsresult rv = SetKeyPoints(aValue, aResult);
if (aParseResult) {
*aParseResult = rv;
}
} else if (aAttribute == nsGkAtoms::rotate) {
nsresult rv = SetRotate(aValue, aResult);
if (aParseResult) {
*aParseResult = rv;
}
} else if (aAttribute == nsGkAtoms::path ||
aAttribute == nsGkAtoms::by ||
aAttribute == nsGkAtoms::from ||
aAttribute == nsGkAtoms::to ||
aAttribute == nsGkAtoms::values) {
aResult.SetTo(aValue);
MarkStaleIfAttributeAffectsPath(aAttribute);
if (aParseResult) {
*aParseResult = NS_OK;
}
} else {
// Defer to superclass method
return nsSMILAnimationFunction::SetAttr(aAttribute, aValue,
aResult, aParseResult);
}
return true;
}
bool
SVGMotionSMILAnimationFunction::UnsetAttr(nsIAtom* aAttribute)
{
if (aAttribute == nsGkAtoms::keyPoints) {
UnsetKeyPoints();
} else if (aAttribute == nsGkAtoms::rotate) {
UnsetRotate();
} else if (aAttribute == nsGkAtoms::path ||
aAttribute == nsGkAtoms::by ||
aAttribute == nsGkAtoms::from ||
aAttribute == nsGkAtoms::to ||
aAttribute == nsGkAtoms::values) {
MarkStaleIfAttributeAffectsPath(aAttribute);
} else {
// Defer to superclass method
return nsSMILAnimationFunction::UnsetAttr(aAttribute);
}
return true;
}
nsSMILAnimationFunction::nsSMILCalcMode
SVGMotionSMILAnimationFunction::GetCalcMode() const
{
const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode);
if (!value) {
return CALC_PACED; // animateMotion defaults to calcMode="paced"
}
return nsSMILCalcMode(value->GetEnumValue());
}
//----------------------------------------------------------------------
// Helpers for GetValues
/*
* Returns the first <mpath> child of the given element
*/
static SVGMPathElement*
GetFirstMPathChild(nsIContent* aElem)
{
for (nsIContent* child = aElem->GetFirstChild();
child;
child = child->GetNextSibling()) {
if (child->IsSVGElement(nsGkAtoms::mpath)) {
return static_cast<SVGMPathElement*>(child);
}
}
return nullptr;
}
void
SVGMotionSMILAnimationFunction::
RebuildPathAndVerticesFromBasicAttrs(const nsIContent* aContextElem)
{
MOZ_ASSERT(!HasAttr(nsGkAtoms::path),
"Should be using |path| attr if we have it");
MOZ_ASSERT(!mPath, "regenerating when we aleady have path");
MOZ_ASSERT(mPathVertices.IsEmpty(),
"regenerating when we already have vertices");
if (!aContextElem->IsSVGElement()) {
NS_ERROR("Uh oh, SVG animateMotion element targeting a non-SVG node");
return;
}
SVGMotionSMILPathUtils::PathGenerator
pathGenerator(static_cast<const nsSVGElement*>(aContextElem));
bool success = false;
if (HasAttr(nsGkAtoms::values)) {
// Generate path based on our values array
mPathSourceType = ePathSourceType_ValuesAttr;
const nsAString& valuesStr = GetAttr(nsGkAtoms::values)->GetStringValue();
SVGMotionSMILPathUtils::MotionValueParser parser(&pathGenerator,
&mPathVertices);
success = nsSMILParserUtils::ParseValuesGeneric(valuesStr, parser);
} else if (HasAttr(nsGkAtoms::to) || HasAttr(nsGkAtoms::by)) {
// Apply 'from' value (or a dummy 0,0 'from' value)
if (HasAttr(nsGkAtoms::from)) {
const nsAString& fromStr = GetAttr(nsGkAtoms::from)->GetStringValue();
success = pathGenerator.MoveToAbsolute(fromStr);
mPathVertices.AppendElement(0.0, fallible);
} else {
// Create dummy 'from' value at 0,0, if we're doing by-animation.
// (NOTE: We don't add the dummy 0-point to our list for *to-animation*,
// because the nsSMILAnimationFunction logic for to-animation doesn't
// expect a dummy value. It only expects one value: the final 'to' value.)
pathGenerator.MoveToOrigin();
if (!HasAttr(nsGkAtoms::to)) {
mPathVertices.AppendElement(0.0, fallible);
}
success = true;
}
// Apply 'to' or 'by' value
if (success) {
double dist;
if (HasAttr(nsGkAtoms::to)) {
mPathSourceType = ePathSourceType_ToAttr;
const nsAString& toStr = GetAttr(nsGkAtoms::to)->GetStringValue();
success = pathGenerator.LineToAbsolute(toStr, dist);
} else { // HasAttr(nsGkAtoms::by)
mPathSourceType = ePathSourceType_ByAttr;
const nsAString& byStr = GetAttr(nsGkAtoms::by)->GetStringValue();
success = pathGenerator.LineToRelative(byStr, dist);
}
if (success) {
mPathVertices.AppendElement(dist, fallible);
}
}
}
if (success) {
mPath = pathGenerator.GetResultingPath();
} else {
// Parse failure. Leave path as null, and clear path-related member data.
mPathVertices.Clear();
}
}
void
SVGMotionSMILAnimationFunction::
RebuildPathAndVerticesFromMpathElem(SVGMPathElement* aMpathElem)
{
mPathSourceType = ePathSourceType_Mpath;
// Use the path that's the target of our chosen <mpath> child.
SVGPathElement* pathElem = aMpathElem->GetReferencedPath();
if (pathElem) {
const SVGPathData &path = pathElem->GetAnimPathSegList()->GetAnimValue();
// Path data must contain of at least one path segment (if the path data
// doesn't begin with a valid "M", then it's invalid).
if (path.Length()) {
bool ok =
path.GetDistancesFromOriginToEndsOfVisibleSegments(&mPathVertices);
if (ok && mPathVertices.Length()) {
mPath = pathElem->GetOrBuildPathForMeasuring();
}
}
}
}
void
SVGMotionSMILAnimationFunction::RebuildPathAndVerticesFromPathAttr()
{
const nsAString& pathSpec = GetAttr(nsGkAtoms::path)->GetStringValue();
mPathSourceType = ePathSourceType_PathAttr;
// Generate Path from |path| attr
SVGPathData path;
nsSVGPathDataParser pathParser(pathSpec, &path);
// We ignore any failure returned from Parse() since the SVG spec says to
// accept all segments up to the first invalid token. Instead we must
// explicitly check that the parse produces at least one path segment (if
// the path data doesn't begin with a valid "M", then it's invalid).
pathParser.Parse();
if (!path.Length()) {
return;
}
mPath = path.BuildPathForMeasuring();
bool ok = path.GetDistancesFromOriginToEndsOfVisibleSegments(&mPathVertices);
if (!ok || !mPathVertices.Length()) {
mPath = nullptr;
}
}
// Helper to regenerate our path representation & its list of vertices
void
SVGMotionSMILAnimationFunction::
RebuildPathAndVertices(const nsIContent* aTargetElement)
{
MOZ_ASSERT(mIsPathStale, "rebuilding path when it isn't stale");
// Clear stale data
mPath = nullptr;
mPathVertices.Clear();
mPathSourceType = ePathSourceType_None;
// Do we have a mpath child? if so, it trumps everything. Otherwise, we look
// through our list of path-defining attributes, in order of priority.
SVGMPathElement* firstMpathChild = GetFirstMPathChild(mAnimationElement);
if (firstMpathChild) {
RebuildPathAndVerticesFromMpathElem(firstMpathChild);
mValueNeedsReparsingEverySample = false;
} else if (HasAttr(nsGkAtoms::path)) {
RebuildPathAndVerticesFromPathAttr();
mValueNeedsReparsingEverySample = false;
} else {
// Get path & vertices from basic SMIL attrs: from/by/to/values
RebuildPathAndVerticesFromBasicAttrs(aTargetElement);
mValueNeedsReparsingEverySample = true;
}
mIsPathStale = false;
}
bool
SVGMotionSMILAnimationFunction::
GenerateValuesForPathAndPoints(Path* aPath,
bool aIsKeyPoints,
FallibleTArray<double>& aPointDistances,
nsSMILValueArray& aResult)
{
MOZ_ASSERT(aResult.IsEmpty(), "outparam is non-empty");
// If we're using "keyPoints" as our list of input distances, then we need
// to de-normalize from the [0, 1] scale to the [0, totalPathLen] scale.
double distanceMultiplier = aIsKeyPoints ? aPath->ComputeLength() : 1.0;
const uint32_t numPoints = aPointDistances.Length();
for (uint32_t i = 0; i < numPoints; ++i) {
double curDist = aPointDistances[i] * distanceMultiplier;
if (!aResult.AppendElement(
SVGMotionSMILType::ConstructSMILValue(aPath, curDist,
mRotateType, mRotateAngle),
fallible)) {
return false;
}
}
return true;
}
nsresult
SVGMotionSMILAnimationFunction::GetValues(const nsISMILAttr& aSMILAttr,
nsSMILValueArray& aResult)
{
if (mIsPathStale) {
RebuildPathAndVertices(aSMILAttr.GetTargetNode());
}
MOZ_ASSERT(!mIsPathStale, "Forgot to clear 'is path stale' state");
if (!mPath) {
// This could be due to e.g. a parse error.
MOZ_ASSERT(mPathVertices.IsEmpty(), "have vertices but no path");
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(!mPathVertices.IsEmpty(), "have a path but no vertices");
// Now: Make the actual list of nsSMILValues (using keyPoints, if set)
bool isUsingKeyPoints = !mKeyPoints.IsEmpty();
bool success = GenerateValuesForPathAndPoints(mPath, isUsingKeyPoints,
isUsingKeyPoints ?
mKeyPoints : mPathVertices,
aResult);
if (!success) {
return NS_ERROR_OUT_OF_MEMORY;
}
return NS_OK;
}
void
SVGMotionSMILAnimationFunction::
CheckValueListDependentAttrs(uint32_t aNumValues)
{
// Call superclass method.
nsSMILAnimationFunction::CheckValueListDependentAttrs(aNumValues);
// Added behavior: Do checks specific to keyPoints.
CheckKeyPoints();
}
bool
SVGMotionSMILAnimationFunction::IsToAnimation() const
{
// Rely on inherited method, but not if we have an <mpath> child or a |path|
// attribute, because they'll override any 'to' attr we might have.
// NOTE: We can't rely on mPathSourceType, because it might not have been
// set to a useful value yet (or it might be stale).
return !GetFirstMPathChild(mAnimationElement) &&
!HasAttr(nsGkAtoms::path) &&
nsSMILAnimationFunction::IsToAnimation();
}
void
SVGMotionSMILAnimationFunction::CheckKeyPoints()
{
if (!HasAttr(nsGkAtoms::keyPoints))
return;
// attribute is ignored for calcMode="paced" (even if it's got errors)
if (GetCalcMode() == CALC_PACED) {
SetKeyPointsErrorFlag(false);
}
if (mKeyPoints.Length() != mKeyTimes.Length()) {
// there must be exactly as many keyPoints as keyTimes
SetKeyPointsErrorFlag(true);
return;
}
// Nothing else to check -- we can catch all keyPoints errors elsewhere.
// - Formatting & range issues will be caught in SetKeyPoints, and will
// result in an empty mKeyPoints array, which will drop us into the error
// case above.
}
nsresult
SVGMotionSMILAnimationFunction::SetKeyPoints(const nsAString& aKeyPoints,
nsAttrValue& aResult)
{
mKeyPoints.Clear();
aResult.SetTo(aKeyPoints);
mHasChanged = true;
if (!nsSMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyPoints, false,
mKeyPoints)) {
mKeyPoints.Clear();
return NS_ERROR_FAILURE;
}
return NS_OK;
}
void
SVGMotionSMILAnimationFunction::UnsetKeyPoints()
{
mKeyPoints.Clear();
SetKeyPointsErrorFlag(false);
mHasChanged = true;
}
nsresult
SVGMotionSMILAnimationFunction::SetRotate(const nsAString& aRotate,
nsAttrValue& aResult)
{
mHasChanged = true;
aResult.SetTo(aRotate);
if (aRotate.EqualsLiteral("auto")) {
mRotateType = eRotateType_Auto;
} else if (aRotate.EqualsLiteral("auto-reverse")) {
mRotateType = eRotateType_AutoReverse;
} else {
mRotateType = eRotateType_Explicit;
// Parse numeric angle string, with the help of a temp nsSVGAngle.
nsSVGAngle svgAngle;
svgAngle.Init();
nsresult rv = svgAngle.SetBaseValueString(aRotate, nullptr, false);
if (NS_FAILED(rv)) { // Parse error
mRotateAngle = 0.0f; // set default rotate angle
// XXX report to console?
return rv;
}
mRotateAngle = svgAngle.GetBaseValInSpecifiedUnits();
// Convert to radian units, if we're not already in radians.
uint8_t angleUnit = svgAngle.GetBaseValueUnit();
if (angleUnit != SVG_ANGLETYPE_RAD) {
mRotateAngle *= nsSVGAngle::GetDegreesPerUnit(angleUnit) /
nsSVGAngle::GetDegreesPerUnit(SVG_ANGLETYPE_RAD);
}
}
return NS_OK;
}
void
SVGMotionSMILAnimationFunction::UnsetRotate()
{
mRotateAngle = 0.0f; // default value
mRotateType = eRotateType_Explicit;
mHasChanged = true;
}
} // namespace mozilla