diff options
Diffstat (limited to '')
-rw-r--r-- | dom/svg/SVGMotionSMILAnimationFunction.cpp | 417 |
1 files changed, 417 insertions, 0 deletions
diff --git a/dom/svg/SVGMotionSMILAnimationFunction.cpp b/dom/svg/SVGMotionSMILAnimationFunction.cpp new file mode 100644 index 0000000000..d950617f0f --- /dev/null +++ b/dom/svg/SVGMotionSMILAnimationFunction.cpp @@ -0,0 +1,417 @@ +/* -*- 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" +#include "mozilla/dom/SVGMPathElement.h" +#include "mozilla/gfx/2D.h" +#include "mozilla/SMILParserUtils.h" +#include "nsAttrValue.h" +#include "nsAttrValueInlines.h" +#include "SVGAnimatedOrient.h" +#include "SVGMotionSMILPathUtils.h" +#include "SVGMotionSMILType.h" +#include "SVGPathDataParser.h" + +using namespace mozilla::dom; +using namespace mozilla::dom::SVGAngle_Binding; +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( + nsAtom* 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 { + MOZ_ASSERT_UNREACHABLE( + "Should only call this method for path-describing " + "attrs"); + isAffected = false; + } + + if (isAffected) { + mIsPathStale = true; + mHasChanged = true; + } +} + +bool SVGMotionSMILAnimationFunction::SetAttr(nsAtom* 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 SMILAnimationFunction::SetAttr(aAttribute, aValue, aResult, + aParseResult); + } + + return true; +} + +bool SVGMotionSMILAnimationFunction::UnsetAttr(nsAtom* 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 SMILAnimationFunction::UnsetAttr(aAttribute); + } + + return true; +} + +SMILAnimationFunction::SMILCalcMode +SVGMotionSMILAnimationFunction::GetCalcMode() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode); + if (!value) { + return CALC_PACED; // animateMotion defaults to calcMode="paced" + } + + return SMILCalcMode(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 already have path"); + MOZ_ASSERT(mPathVertices.IsEmpty(), + "regenerating when we already have vertices"); + + const auto* context = SVGElement::FromNode(aContextElem); + if (!context) { + NS_ERROR("Uh oh, SVG animateMotion element targeting a non-SVG node"); + return; + } + SVGMotionSMILPathUtils::PathGenerator pathGenerator(context); + + 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 = SMILParserUtils::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); + if (!mPathVertices.AppendElement(0.0, fallible)) { + success = false; + } + } 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 SMILAnimationFunction logic for to-animation doesn't + // expect a dummy value. It only expects one value: the final 'to' value.) + pathGenerator.MoveToOrigin(); + success = true; + if (!HasAttr(nsGkAtoms::to)) { + if (!mPathVertices.AppendElement(0.0, fallible)) { + success = false; + } + } + } + + // 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) { + if (!mPathVertices.AppendElement(dist, fallible)) { + success = false; + } + } + } + } + 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 shape that's the target of our chosen <mpath> child. + SVGGeometryElement* shapeElem = aMpathElem->GetReferencedPath(); + if (shapeElem && shapeElem->HasValidDimensions()) { + bool ok = shapeElem->GetDistancesFromOriginToEndsOfVisibleSegments( + &mPathVertices); + if (ok && mPathVertices.Length()) { + mPath = shapeElem->GetOrBuildPathForMeasuring(); + } + } +} + +void SVGMotionSMILAnimationFunction::RebuildPathAndVerticesFromPathAttr() { + const nsAString& pathSpec = GetAttr(nsGkAtoms::path)->GetStringValue(); + mPathSourceType = ePathSourceType_PathAttr; + + // Generate Path from |path| attr + SVGPathData path; + SVGPathDataParser 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; + mPathVertices.Clear(); + } +} + +// 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, + SMILValueArray& 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 SMILAttr& aSMILAttr, + SMILValueArray& 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 SMILValues (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. + SMILAnimationFunction::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) && + SMILAnimationFunction::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 (!SMILParserUtils::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; + + uint16_t angleUnit; + if (!SVGAnimatedOrient::GetValueFromString(aRotate, mRotateAngle, + &angleUnit)) { + mRotateAngle = 0.0f; // set default rotate angle + // XXX report to console? + return NS_ERROR_DOM_SYNTAX_ERR; + } + + // Convert to radian units, if we're not already in radians. + if (angleUnit != SVG_ANGLETYPE_RAD) { + mRotateAngle *= SVGAnimatedOrient::GetDegreesPerUnit(angleUnit) / + SVGAnimatedOrient::GetDegreesPerUnit(SVG_ANGLETYPE_RAD); + } + } + return NS_OK; +} + +void SVGMotionSMILAnimationFunction::UnsetRotate() { + mRotateAngle = 0.0f; // default value + mRotateType = eRotateType_Explicit; + mHasChanged = true; +} + +} // namespace mozilla |