From 5f8de423f190bbb79a62f804151bc24824fa32d8 Mon Sep 17 00:00:00 2001 From: "Matt A. Tobin" Date: Fri, 2 Feb 2018 04:16:08 -0500 Subject: Add m-esr52 at 52.6.0 --- dom/smil/SMILBoolType.cpp | 83 + dom/smil/SMILBoolType.h | 50 + dom/smil/SMILEnumType.cpp | 83 + dom/smil/SMILEnumType.h | 51 + dom/smil/SMILIntegerType.cpp | 100 + dom/smil/SMILIntegerType.h | 46 + dom/smil/SMILStringType.cpp | 91 + dom/smil/SMILStringType.h | 51 + dom/smil/TimeEvent.cpp | 79 + dom/smil/TimeEvent.h | 71 + dom/smil/crashtests/1010681-1.svg | 23 + dom/smil/crashtests/483584-1.svg | 8 + dom/smil/crashtests/483584-2.svg | 133 ++ dom/smil/crashtests/523188-1.svg | 15 + dom/smil/crashtests/525099-1.svg | 7 + dom/smil/crashtests/526536-1.svg | 19 + dom/smil/crashtests/526875-1.svg | 4 + dom/smil/crashtests/526875-2.svg | 4 + dom/smil/crashtests/529387-1-helper.svg | 5 + dom/smil/crashtests/529387-1.xhtml | 7 + dom/smil/crashtests/531550-1.svg | 3 + dom/smil/crashtests/537157-1.svg | 11 + dom/smil/crashtests/541297-1.svg | 22 + dom/smil/crashtests/547333-1.svg | 22 + dom/smil/crashtests/548899-1.svg | 14 + dom/smil/crashtests/551620-1.svg | 21 + dom/smil/crashtests/554141-1.svg | 12 + dom/smil/crashtests/554202-1.svg | 31 + dom/smil/crashtests/554202-2.svg | 19 + dom/smil/crashtests/555026-1.svg | 25 + dom/smil/crashtests/556841-1.svg | 16 + dom/smil/crashtests/572938-1.svg | 12 + dom/smil/crashtests/572938-2.svg | 22 + dom/smil/crashtests/572938-3.svg | 10 + dom/smil/crashtests/572938-4.svg | 10 + dom/smil/crashtests/588287-1.svg | 24 + dom/smil/crashtests/588287-2.svg | 26 + dom/smil/crashtests/590425-1.html | 24 + dom/smil/crashtests/592477-1.xhtml | 26 + dom/smil/crashtests/594653-1.svg | 26 + dom/smil/crashtests/596796-1.svg | 15 + dom/smil/crashtests/605345-1.svg | 25 + dom/smil/crashtests/606101-1.svg | 23 + dom/smil/crashtests/608295-1.html | 18 + dom/smil/crashtests/608549-1.svg | 29 + dom/smil/crashtests/611927-1.svg | 4 + dom/smil/crashtests/615002-1.svg | 16 + dom/smil/crashtests/615872-1.svg | 21 + dom/smil/crashtests/641388-1.html | 98 + dom/smil/crashtests/641388-2.html | 79 + dom/smil/crashtests/650732-1.svg | 46 + dom/smil/crashtests/665334-1.svg | 13 + dom/smil/crashtests/669225-1.svg | 21 + dom/smil/crashtests/669225-2.svg | 21 + dom/smil/crashtests/670313-1.svg | 20 + dom/smil/crashtests/678822-1.svg | 3 + dom/smil/crashtests/678847-1.svg | 3 + dom/smil/crashtests/678938-1.svg | 11 + dom/smil/crashtests/690994-1.svg | 17 + dom/smil/crashtests/691337-1.svg | 8 + dom/smil/crashtests/691337-2.svg | 11 + dom/smil/crashtests/697640-1.svg | 3 + dom/smil/crashtests/699325-1.svg | 5 + dom/smil/crashtests/709907-1.svg | 3 + dom/smil/crashtests/720103-1.svg | 4 + dom/smil/crashtests/crashtests.list | 54 + dom/smil/moz.build | 72 + dom/smil/nsISMILAttr.h | 98 + dom/smil/nsISMILType.h | 214 ++ dom/smil/nsSMILAnimationController.cpp | 795 +++++++ dom/smil/nsSMILAnimationController.h | 211 ++ dom/smil/nsSMILAnimationFunction.cpp | 1070 +++++++++ dom/smil/nsSMILAnimationFunction.h | 458 ++++ dom/smil/nsSMILCSSProperty.cpp | 275 +++ dom/smil/nsSMILCSSProperty.h | 67 + dom/smil/nsSMILCSSValueType.cpp | 447 ++++ dom/smil/nsSMILCSSValueType.h | 116 + dom/smil/nsSMILCompositor.cpp | 204 ++ dom/smil/nsSMILCompositor.h | 107 + dom/smil/nsSMILCompositorTable.h | 23 + dom/smil/nsSMILFloatType.cpp | 92 + dom/smil/nsSMILFloatType.h | 47 + dom/smil/nsSMILInstanceTime.cpp | 212 ++ dom/smil/nsSMILInstanceTime.h | 166 ++ dom/smil/nsSMILInterval.cpp | 170 ++ dom/smil/nsSMILInterval.h | 86 + dom/smil/nsSMILKeySpline.cpp | 151 ++ dom/smil/nsSMILKeySpline.h | 122 + dom/smil/nsSMILMappedAttribute.cpp | 150 ++ dom/smil/nsSMILMappedAttribute.h | 56 + dom/smil/nsSMILMilestone.h | 79 + dom/smil/nsSMILNullType.cpp | 56 + dom/smil/nsSMILNullType.h | 50 + dom/smil/nsSMILParserUtils.cpp | 730 ++++++ dom/smil/nsSMILParserUtils.h | 89 + dom/smil/nsSMILRepeatCount.cpp | 10 + dom/smil/nsSMILRepeatCount.h | 62 + dom/smil/nsSMILSetAnimationFunction.cpp | 101 + dom/smil/nsSMILSetAnimationFunction.h | 68 + dom/smil/nsSMILTargetIdentifier.h | 86 + dom/smil/nsSMILTimeContainer.cpp | 339 +++ dom/smil/nsSMILTimeContainer.h | 298 +++ dom/smil/nsSMILTimeValue.cpp | 41 + dom/smil/nsSMILTimeValue.h | 135 ++ dom/smil/nsSMILTimeValueSpec.cpp | 536 +++++ dom/smil/nsSMILTimeValueSpec.h | 131 ++ dom/smil/nsSMILTimeValueSpecParams.h | 68 + dom/smil/nsSMILTimedElement.cpp | 2444 ++++++++++++++++++++ dom/smil/nsSMILTimedElement.h | 673 ++++++ dom/smil/nsSMILTypes.h | 26 + dom/smil/nsSMILValue.cpp | 162 ++ dom/smil/nsSMILValue.h | 81 + dom/smil/test/db_smilAnimateMotion.js | 253 ++ dom/smil/test/db_smilCSSFromBy.js | 166 ++ dom/smil/test/db_smilCSSFromTo.js | 483 ++++ dom/smil/test/db_smilCSSPaced.js | 321 +++ dom/smil/test/db_smilCSSPropertyList.js | 93 + dom/smil/test/db_smilMappedAttrList.js | 131 ++ dom/smil/test/mochitest.ini | 60 + dom/smil/test/smilAnimateMotionValueLists.js | 128 + dom/smil/test/smilExtDoc_helper.svg | 7 + dom/smil/test/smilTestUtils.js | 858 +++++++ dom/smil/test/smilXHR_helper.svg | 8 + dom/smil/test/test_smilAccessKey.xhtml | 362 +++ dom/smil/test/test_smilAnimateMotion.xhtml | 51 + .../test/test_smilAnimateMotionInvalidValues.xhtml | 176 ++ .../test/test_smilAnimateMotionOverrideRules.xhtml | 215 ++ dom/smil/test/test_smilBackwardsSeeking.xhtml | 191 ++ .../test/test_smilCSSFontStretchRelative.xhtml | 102 + dom/smil/test/test_smilCSSFromBy.xhtml | 49 + dom/smil/test/test_smilCSSFromTo.xhtml | 76 + dom/smil/test/test_smilCSSInherit.xhtml | 85 + dom/smil/test/test_smilCSSInvalidValues.xhtml | 59 + dom/smil/test/test_smilCSSPaced.xhtml | 44 + dom/smil/test/test_smilChangeAfterFrozen.xhtml | 571 +++++ dom/smil/test/test_smilConditionalProcessing.html | 80 + dom/smil/test/test_smilContainerBinding.xhtml | 101 + dom/smil/test/test_smilCrossContainer.xhtml | 132 ++ .../test/test_smilDynamicDelayedBeginElement.xhtml | 103 + dom/smil/test/test_smilExtDoc.xhtml | 80 + dom/smil/test/test_smilFillMode.xhtml | 86 + dom/smil/test/test_smilGetSimpleDuration.xhtml | 86 + dom/smil/test/test_smilGetStartTime.xhtml | 103 + dom/smil/test/test_smilHyperlinking.xhtml | 233 ++ dom/smil/test/test_smilInvalidValues.html | 113 + dom/smil/test/test_smilKeySplines.xhtml | 296 +++ dom/smil/test/test_smilKeyTimes.xhtml | 391 ++++ dom/smil/test/test_smilKeyTimesPacedMode.xhtml | 123 + dom/smil/test/test_smilMappedAttrFromBy.xhtml | 51 + dom/smil/test/test_smilMappedAttrFromTo.xhtml | 79 + dom/smil/test/test_smilMappedAttrPaced.xhtml | 46 + dom/smil/test/test_smilMinTiming.html | 93 + dom/smil/test/test_smilRepeatDuration.html | 139 ++ dom/smil/test/test_smilRepeatTiming.xhtml | 96 + dom/smil/test/test_smilReset.xhtml | 82 + dom/smil/test/test_smilRestart.xhtml | 102 + dom/smil/test/test_smilSetCurrentTime.xhtml | 76 + dom/smil/test/test_smilSync.xhtml | 255 ++ dom/smil/test/test_smilSyncTransform.xhtml | 66 + dom/smil/test/test_smilSyncbaseTarget.xhtml | 180 ++ dom/smil/test/test_smilTextZoom.xhtml | 89 + dom/smil/test/test_smilTimeEvents.xhtml | 337 +++ dom/smil/test/test_smilTiming.xhtml | 291 +++ dom/smil/test/test_smilTimingZeroIntervals.xhtml | 285 +++ dom/smil/test/test_smilUpdatedInterval.xhtml | 64 + dom/smil/test/test_smilValues.xhtml | 171 ++ dom/smil/test/test_smilXHR.xhtml | 88 + 167 files changed, 22557 insertions(+) create mode 100644 dom/smil/SMILBoolType.cpp create mode 100644 dom/smil/SMILBoolType.h create mode 100644 dom/smil/SMILEnumType.cpp create mode 100644 dom/smil/SMILEnumType.h create mode 100644 dom/smil/SMILIntegerType.cpp create mode 100644 dom/smil/SMILIntegerType.h create mode 100644 dom/smil/SMILStringType.cpp create mode 100644 dom/smil/SMILStringType.h create mode 100644 dom/smil/TimeEvent.cpp create mode 100644 dom/smil/TimeEvent.h create mode 100644 dom/smil/crashtests/1010681-1.svg create mode 100644 dom/smil/crashtests/483584-1.svg create mode 100644 dom/smil/crashtests/483584-2.svg create mode 100644 dom/smil/crashtests/523188-1.svg create mode 100644 dom/smil/crashtests/525099-1.svg create mode 100644 dom/smil/crashtests/526536-1.svg create mode 100644 dom/smil/crashtests/526875-1.svg create mode 100644 dom/smil/crashtests/526875-2.svg create mode 100644 dom/smil/crashtests/529387-1-helper.svg create mode 100644 dom/smil/crashtests/529387-1.xhtml create mode 100644 dom/smil/crashtests/531550-1.svg create mode 100644 dom/smil/crashtests/537157-1.svg create mode 100644 dom/smil/crashtests/541297-1.svg create mode 100644 dom/smil/crashtests/547333-1.svg create mode 100644 dom/smil/crashtests/548899-1.svg create mode 100644 dom/smil/crashtests/551620-1.svg create mode 100644 dom/smil/crashtests/554141-1.svg create mode 100644 dom/smil/crashtests/554202-1.svg create mode 100644 dom/smil/crashtests/554202-2.svg create mode 100644 dom/smil/crashtests/555026-1.svg create mode 100644 dom/smil/crashtests/556841-1.svg create mode 100644 dom/smil/crashtests/572938-1.svg create mode 100644 dom/smil/crashtests/572938-2.svg create mode 100644 dom/smil/crashtests/572938-3.svg create mode 100644 dom/smil/crashtests/572938-4.svg create mode 100644 dom/smil/crashtests/588287-1.svg create mode 100644 dom/smil/crashtests/588287-2.svg create mode 100644 dom/smil/crashtests/590425-1.html create mode 100644 dom/smil/crashtests/592477-1.xhtml create mode 100644 dom/smil/crashtests/594653-1.svg create mode 100644 dom/smil/crashtests/596796-1.svg create mode 100644 dom/smil/crashtests/605345-1.svg create mode 100644 dom/smil/crashtests/606101-1.svg create mode 100644 dom/smil/crashtests/608295-1.html create mode 100644 dom/smil/crashtests/608549-1.svg create mode 100644 dom/smil/crashtests/611927-1.svg create mode 100644 dom/smil/crashtests/615002-1.svg create mode 100644 dom/smil/crashtests/615872-1.svg create mode 100644 dom/smil/crashtests/641388-1.html create mode 100644 dom/smil/crashtests/641388-2.html create mode 100644 dom/smil/crashtests/650732-1.svg create mode 100644 dom/smil/crashtests/665334-1.svg create mode 100644 dom/smil/crashtests/669225-1.svg create mode 100644 dom/smil/crashtests/669225-2.svg create mode 100644 dom/smil/crashtests/670313-1.svg create mode 100644 dom/smil/crashtests/678822-1.svg create mode 100644 dom/smil/crashtests/678847-1.svg create mode 100644 dom/smil/crashtests/678938-1.svg create mode 100644 dom/smil/crashtests/690994-1.svg create mode 100644 dom/smil/crashtests/691337-1.svg create mode 100644 dom/smil/crashtests/691337-2.svg create mode 100644 dom/smil/crashtests/697640-1.svg create mode 100644 dom/smil/crashtests/699325-1.svg create mode 100644 dom/smil/crashtests/709907-1.svg create mode 100644 dom/smil/crashtests/720103-1.svg create mode 100644 dom/smil/crashtests/crashtests.list create mode 100644 dom/smil/moz.build create mode 100644 dom/smil/nsISMILAttr.h create mode 100644 dom/smil/nsISMILType.h create mode 100644 dom/smil/nsSMILAnimationController.cpp create mode 100644 dom/smil/nsSMILAnimationController.h create mode 100644 dom/smil/nsSMILAnimationFunction.cpp create mode 100644 dom/smil/nsSMILAnimationFunction.h create mode 100644 dom/smil/nsSMILCSSProperty.cpp create mode 100644 dom/smil/nsSMILCSSProperty.h create mode 100644 dom/smil/nsSMILCSSValueType.cpp create mode 100644 dom/smil/nsSMILCSSValueType.h create mode 100644 dom/smil/nsSMILCompositor.cpp create mode 100644 dom/smil/nsSMILCompositor.h create mode 100644 dom/smil/nsSMILCompositorTable.h create mode 100644 dom/smil/nsSMILFloatType.cpp create mode 100644 dom/smil/nsSMILFloatType.h create mode 100644 dom/smil/nsSMILInstanceTime.cpp create mode 100644 dom/smil/nsSMILInstanceTime.h create mode 100644 dom/smil/nsSMILInterval.cpp create mode 100644 dom/smil/nsSMILInterval.h create mode 100644 dom/smil/nsSMILKeySpline.cpp create mode 100644 dom/smil/nsSMILKeySpline.h create mode 100644 dom/smil/nsSMILMappedAttribute.cpp create mode 100644 dom/smil/nsSMILMappedAttribute.h create mode 100644 dom/smil/nsSMILMilestone.h create mode 100644 dom/smil/nsSMILNullType.cpp create mode 100644 dom/smil/nsSMILNullType.h create mode 100644 dom/smil/nsSMILParserUtils.cpp create mode 100644 dom/smil/nsSMILParserUtils.h create mode 100644 dom/smil/nsSMILRepeatCount.cpp create mode 100644 dom/smil/nsSMILRepeatCount.h create mode 100644 dom/smil/nsSMILSetAnimationFunction.cpp create mode 100644 dom/smil/nsSMILSetAnimationFunction.h create mode 100644 dom/smil/nsSMILTargetIdentifier.h create mode 100644 dom/smil/nsSMILTimeContainer.cpp create mode 100644 dom/smil/nsSMILTimeContainer.h create mode 100644 dom/smil/nsSMILTimeValue.cpp create mode 100644 dom/smil/nsSMILTimeValue.h create mode 100644 dom/smil/nsSMILTimeValueSpec.cpp create mode 100644 dom/smil/nsSMILTimeValueSpec.h create mode 100644 dom/smil/nsSMILTimeValueSpecParams.h create mode 100644 dom/smil/nsSMILTimedElement.cpp create mode 100644 dom/smil/nsSMILTimedElement.h create mode 100644 dom/smil/nsSMILTypes.h create mode 100644 dom/smil/nsSMILValue.cpp create mode 100644 dom/smil/nsSMILValue.h create mode 100644 dom/smil/test/db_smilAnimateMotion.js create mode 100644 dom/smil/test/db_smilCSSFromBy.js create mode 100644 dom/smil/test/db_smilCSSFromTo.js create mode 100644 dom/smil/test/db_smilCSSPaced.js create mode 100644 dom/smil/test/db_smilCSSPropertyList.js create mode 100644 dom/smil/test/db_smilMappedAttrList.js create mode 100644 dom/smil/test/mochitest.ini create mode 100644 dom/smil/test/smilAnimateMotionValueLists.js create mode 100644 dom/smil/test/smilExtDoc_helper.svg create mode 100644 dom/smil/test/smilTestUtils.js create mode 100644 dom/smil/test/smilXHR_helper.svg create mode 100644 dom/smil/test/test_smilAccessKey.xhtml create mode 100644 dom/smil/test/test_smilAnimateMotion.xhtml create mode 100644 dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml create mode 100644 dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml create mode 100644 dom/smil/test/test_smilBackwardsSeeking.xhtml create mode 100644 dom/smil/test/test_smilCSSFontStretchRelative.xhtml create mode 100644 dom/smil/test/test_smilCSSFromBy.xhtml create mode 100644 dom/smil/test/test_smilCSSFromTo.xhtml create mode 100644 dom/smil/test/test_smilCSSInherit.xhtml create mode 100644 dom/smil/test/test_smilCSSInvalidValues.xhtml create mode 100644 dom/smil/test/test_smilCSSPaced.xhtml create mode 100644 dom/smil/test/test_smilChangeAfterFrozen.xhtml create mode 100644 dom/smil/test/test_smilConditionalProcessing.html create mode 100644 dom/smil/test/test_smilContainerBinding.xhtml create mode 100644 dom/smil/test/test_smilCrossContainer.xhtml create mode 100644 dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml create mode 100644 dom/smil/test/test_smilExtDoc.xhtml create mode 100644 dom/smil/test/test_smilFillMode.xhtml create mode 100644 dom/smil/test/test_smilGetSimpleDuration.xhtml create mode 100644 dom/smil/test/test_smilGetStartTime.xhtml create mode 100644 dom/smil/test/test_smilHyperlinking.xhtml create mode 100644 dom/smil/test/test_smilInvalidValues.html create mode 100644 dom/smil/test/test_smilKeySplines.xhtml create mode 100644 dom/smil/test/test_smilKeyTimes.xhtml create mode 100644 dom/smil/test/test_smilKeyTimesPacedMode.xhtml create mode 100644 dom/smil/test/test_smilMappedAttrFromBy.xhtml create mode 100644 dom/smil/test/test_smilMappedAttrFromTo.xhtml create mode 100644 dom/smil/test/test_smilMappedAttrPaced.xhtml create mode 100644 dom/smil/test/test_smilMinTiming.html create mode 100644 dom/smil/test/test_smilRepeatDuration.html create mode 100644 dom/smil/test/test_smilRepeatTiming.xhtml create mode 100644 dom/smil/test/test_smilReset.xhtml create mode 100644 dom/smil/test/test_smilRestart.xhtml create mode 100644 dom/smil/test/test_smilSetCurrentTime.xhtml create mode 100644 dom/smil/test/test_smilSync.xhtml create mode 100644 dom/smil/test/test_smilSyncTransform.xhtml create mode 100644 dom/smil/test/test_smilSyncbaseTarget.xhtml create mode 100644 dom/smil/test/test_smilTextZoom.xhtml create mode 100644 dom/smil/test/test_smilTimeEvents.xhtml create mode 100644 dom/smil/test/test_smilTiming.xhtml create mode 100644 dom/smil/test/test_smilTimingZeroIntervals.xhtml create mode 100644 dom/smil/test/test_smilUpdatedInterval.xhtml create mode 100644 dom/smil/test/test_smilValues.xhtml create mode 100644 dom/smil/test/test_smilXHR.xhtml (limited to 'dom/smil') diff --git a/dom/smil/SMILBoolType.cpp b/dom/smil/SMILBoolType.cpp new file mode 100644 index 000000000..f6ae19b79 --- /dev/null +++ b/dom/smil/SMILBoolType.cpp @@ -0,0 +1,83 @@ +/* -*- 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 "SMILBoolType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include + +namespace mozilla { + +void +SMILBoolType::Init(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mBool = false; + aValue.mType = this; +} + +void +SMILBoolType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mBool = false; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +SMILBoolType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mBool = aSrc.mU.mBool; + return NS_OK; +} + +bool +SMILBoolType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mBool == aRight.mU.mBool; +} + +nsresult +SMILBoolType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // bool values can't be added to each other +} + +nsresult +SMILBoolType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between bool values +} + +nsresult +SMILBoolType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // bool values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILBoolType.h b/dom/smil/SMILBoolType.h new file mode 100644 index 000000000..608a09ccf --- /dev/null +++ b/dom/smil/SMILBoolType.h @@ -0,0 +1,50 @@ +/* -*- 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/. */ + +#ifndef MOZILLA_SMILBOOLTYPE_H_ +#define MOZILLA_SMILBOOLTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +namespace mozilla { + +class SMILBoolType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static SMILBoolType* Singleton() + { + static SMILBoolType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILBoolType() {} +}; + +} // namespace mozilla + +#endif // MOZILLA_SMILBOOLTYPE_H_ diff --git a/dom/smil/SMILEnumType.cpp b/dom/smil/SMILEnumType.cpp new file mode 100644 index 000000000..2aa7a04c1 --- /dev/null +++ b/dom/smil/SMILEnumType.cpp @@ -0,0 +1,83 @@ +/* -*- 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 "SMILEnumType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include + +namespace mozilla { + +void +SMILEnumType::Init(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mUint = 0; + aValue.mType = this; +} + +void +SMILEnumType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mUint = 0; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +SMILEnumType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mUint = aSrc.mU.mUint; + return NS_OK; +} + +bool +SMILEnumType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mUint == aRight.mU.mUint; +} + +nsresult +SMILEnumType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // enum values can't be added to each other +} + +nsresult +SMILEnumType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between enum values +} + +nsresult +SMILEnumType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // enum values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILEnumType.h b/dom/smil/SMILEnumType.h new file mode 100644 index 000000000..b6cda3ff9 --- /dev/null +++ b/dom/smil/SMILEnumType.h @@ -0,0 +1,51 @@ +/* -*- 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/. */ + +#ifndef MOZILLA_SMILENUMTYPE_H_ +#define MOZILLA_SMILENUMTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +namespace mozilla { + +class SMILEnumType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static SMILEnumType* + Singleton() + { + static SMILEnumType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILEnumType() {} +}; + +} // namespace mozilla + +#endif // MOZILLA_SMILENUMTYPE_H_ diff --git a/dom/smil/SMILIntegerType.cpp b/dom/smil/SMILIntegerType.cpp new file mode 100644 index 000000000..194653e1b --- /dev/null +++ b/dom/smil/SMILIntegerType.cpp @@ -0,0 +1,100 @@ +/* -*- 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 "SMILIntegerType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include + +namespace mozilla { + +void +SMILIntegerType::Init(nsSMILValue& aValue) const +{ + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mInt = 0; + aValue.mType = this; +} + +void +SMILIntegerType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mInt = 0; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +SMILIntegerType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mInt = aSrc.mU.mInt; + return NS_OK; +} + +bool +SMILIntegerType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mInt == aRight.mU.mInt; +} + +nsresult +SMILIntegerType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + aDest.mU.mInt += aValueToAdd.mU.mInt * aCount; + return NS_OK; +} + +nsresult +SMILIntegerType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + aDistance = fabs(double(aTo.mU.mInt - aFrom.mU.mInt)); + return NS_OK; +} + +nsresult +SMILIntegerType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + + const double startVal = double(aStartVal.mU.mInt); + const double endVal = double(aEndVal.mU.mInt); + const double currentVal = startVal + (endVal - startVal) * aUnitDistance; + + // When currentVal is exactly midway between its two nearest integers, we + // jump to the "next" integer to provide simple, easy to remember and + // consistent behaviour (from the SMIL author's point of view). + + if (startVal < endVal) { + aResult.mU.mInt = int64_t(floor(currentVal + 0.5)); // round mid up + } else { + aResult.mU.mInt = int64_t(ceil(currentVal - 0.5)); // round mid down + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/smil/SMILIntegerType.h b/dom/smil/SMILIntegerType.h new file mode 100644 index 000000000..39560cc79 --- /dev/null +++ b/dom/smil/SMILIntegerType.h @@ -0,0 +1,46 @@ +/* -*- 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/. */ + +#ifndef MOZILLA_SMILINTEGERTYPE_H_ +#define MOZILLA_SMILINTEGERTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +namespace mozilla { + +class SMILIntegerType : public nsISMILType +{ +public: + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + + static SMILIntegerType* + Singleton() + { + static SMILIntegerType sSingleton; + return &sSingleton; + } + +private: + constexpr SMILIntegerType() {} +}; + +} // namespace mozilla + +#endif // MOZILLA_SMILINTEGERTYPE_H_ diff --git a/dom/smil/SMILStringType.cpp b/dom/smil/SMILStringType.cpp new file mode 100644 index 000000000..d67323b7e --- /dev/null +++ b/dom/smil/SMILStringType.cpp @@ -0,0 +1,91 @@ +/* -*- 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 "SMILStringType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include "nsString.h" + +namespace mozilla { + +void +SMILStringType::Init(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mPtr = new nsString(); + aValue.mType = this; +} + +void +SMILStringType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + delete static_cast(aValue.mU.mPtr); + aValue.mU.mPtr = nullptr; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +SMILStringType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + + const nsAString* src = static_cast(aSrc.mU.mPtr); + nsAString* dst = static_cast(aDest.mU.mPtr); + *dst = *src; + return NS_OK; +} + +bool +SMILStringType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + const nsAString* leftString = + static_cast(aLeft.mU.mPtr); + const nsAString* rightString = + static_cast(aRight.mU.mPtr); + return *leftString == *rightString; +} + +nsresult +SMILStringType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // string values can't be added to each other +} + +nsresult +SMILStringType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between string values +} + +nsresult +SMILStringType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // string values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILStringType.h b/dom/smil/SMILStringType.h new file mode 100644 index 000000000..6f160cadb --- /dev/null +++ b/dom/smil/SMILStringType.h @@ -0,0 +1,51 @@ +/* -*- 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/. */ + +#ifndef MOZILLA_SMILSTRINGTYPE_H_ +#define MOZILLA_SMILSTRINGTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +namespace mozilla { + +class SMILStringType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static SMILStringType* + Singleton() + { + static SMILStringType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILStringType() {} +}; + +} // namespace mozilla + +#endif // MOZILLA_SMILSTRINGTYPE_H_ diff --git a/dom/smil/TimeEvent.cpp b/dom/smil/TimeEvent.cpp new file mode 100644 index 000000000..8a58c6750 --- /dev/null +++ b/dom/smil/TimeEvent.cpp @@ -0,0 +1,79 @@ +/* -*- 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 "mozilla/ContentEvents.h" +#include "mozilla/dom/TimeEvent.h" +#include "nsIDocShell.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsPresContext.h" +#include "nsGlobalWindow.h" + +namespace mozilla { +namespace dom { + +TimeEvent::TimeEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent) + : Event(aOwner, aPresContext, + aEvent ? aEvent : new InternalSMILTimeEvent(false, eVoidEvent)) + , mDetail(mEvent->AsSMILTimeEvent()->mDetail) +{ + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } + + if (mPresContext) { + nsCOMPtr docShell = mPresContext->GetDocShell(); + if (docShell) { + mView = docShell->GetWindow(); + } + } +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(TimeEvent, Event, + mView) + +NS_IMPL_ADDREF_INHERITED(TimeEvent, Event) +NS_IMPL_RELEASE_INHERITED(TimeEvent, Event) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TimeEvent) + NS_INTERFACE_MAP_ENTRY(nsIDOMTimeEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMETHODIMP +TimeEvent::GetDetail(int32_t* aDetail) +{ + *aDetail = mDetail; + return NS_OK; +} + +void +TimeEvent::InitTimeEvent(const nsAString& aType, nsGlobalWindow* aView, + int32_t aDetail) +{ + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Event::InitEvent(aType, false /*doesn't bubble*/, false /*can't cancel*/); + mDetail = aDetail; + mView = aView ? aView->GetOuterWindow() : nullptr; +} + +} // namespace dom +} // namespace mozilla + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed +NS_NewDOMTimeEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent) +{ + RefPtr it = new TimeEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/smil/TimeEvent.h b/dom/smil/TimeEvent.h new file mode 100644 index 000000000..b5af5747e --- /dev/null +++ b/dom/smil/TimeEvent.h @@ -0,0 +1,71 @@ +/* -*- 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/. */ + +#ifndef mozilla_dom_TimeEvent_h_ +#define mozilla_dom_TimeEvent_h_ + +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TimeEventBinding.h" +#include "nsIDOMTimeEvent.h" + +class nsGlobalWindow; + +namespace mozilla { +namespace dom { + +class TimeEvent final : public Event, + public nsIDOMTimeEvent +{ +public: + TimeEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent); + + // nsISupports interface: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TimeEvent, Event) + + // nsIDOMTimeEvent interface: + NS_DECL_NSIDOMTIMEEVENT + + // Forward to base class + NS_FORWARD_TO_EVENT + + virtual JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle aGivenProto) override + { + return TimeEventBinding::Wrap(aCx, this, aGivenProto); + } + + void InitTimeEvent(const nsAString& aType, nsGlobalWindow* aView, + int32_t aDetail); + + + int32_t Detail() const + { + return mDetail; + } + + nsPIDOMWindowOuter* GetView() const + { + return mView; + } + +private: + ~TimeEvent() {} + + nsCOMPtr mView; + int32_t mDetail; +}; + +} // namespace dom +} // namespace mozilla + +already_AddRefed +NS_NewDOMTimeEvent(mozilla::dom::EventTarget* aOwner, + nsPresContext* aPresContext, + mozilla::InternalSMILTimeEvent* aEvent); + +#endif // mozilla_dom_TimeEvent_h_ diff --git a/dom/smil/crashtests/1010681-1.svg b/dom/smil/crashtests/1010681-1.svg new file mode 100644 index 000000000..5dd394472 --- /dev/null +++ b/dom/smil/crashtests/1010681-1.svg @@ -0,0 +1,23 @@ + + + diff --git a/dom/smil/crashtests/483584-1.svg b/dom/smil/crashtests/483584-1.svg new file mode 100644 index 000000000..b9ded113e --- /dev/null +++ b/dom/smil/crashtests/483584-1.svg @@ -0,0 +1,8 @@ + + + + + + + diff --git a/dom/smil/crashtests/483584-2.svg b/dom/smil/crashtests/483584-2.svg new file mode 100644 index 000000000..f5cbd7d46 --- /dev/null +++ b/dom/smil/crashtests/483584-2.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + $Revision: 1.6 $ + + diff --git a/dom/smil/crashtests/523188-1.svg b/dom/smil/crashtests/523188-1.svg new file mode 100644 index 000000000..c03cea492 --- /dev/null +++ b/dom/smil/crashtests/523188-1.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/dom/smil/crashtests/525099-1.svg b/dom/smil/crashtests/525099-1.svg new file mode 100644 index 000000000..8eed11489 --- /dev/null +++ b/dom/smil/crashtests/525099-1.svg @@ -0,0 +1,7 @@ + + + + + + diff --git a/dom/smil/crashtests/526536-1.svg b/dom/smil/crashtests/526536-1.svg new file mode 100644 index 000000000..4fcf35d08 --- /dev/null +++ b/dom/smil/crashtests/526536-1.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/dom/smil/crashtests/526875-1.svg b/dom/smil/crashtests/526875-1.svg new file mode 100644 index 000000000..281454bf6 --- /dev/null +++ b/dom/smil/crashtests/526875-1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dom/smil/crashtests/526875-2.svg b/dom/smil/crashtests/526875-2.svg new file mode 100644 index 000000000..73c229da5 --- /dev/null +++ b/dom/smil/crashtests/526875-2.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dom/smil/crashtests/529387-1-helper.svg b/dom/smil/crashtests/529387-1-helper.svg new file mode 100644 index 000000000..7885ab71f --- /dev/null +++ b/dom/smil/crashtests/529387-1-helper.svg @@ -0,0 +1,5 @@ + + abc + + + diff --git a/dom/smil/crashtests/529387-1.xhtml b/dom/smil/crashtests/529387-1.xhtml new file mode 100644 index 000000000..de3dbec34 --- /dev/null +++ b/dom/smil/crashtests/529387-1.xhtml @@ -0,0 +1,7 @@ + + + diff --git a/dom/smil/crashtests/531550-1.svg b/dom/smil/crashtests/531550-1.svg new file mode 100644 index 000000000..306f41702 --- /dev/null +++ b/dom/smil/crashtests/531550-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/537157-1.svg b/dom/smil/crashtests/537157-1.svg new file mode 100644 index 000000000..df615ab9a --- /dev/null +++ b/dom/smil/crashtests/537157-1.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/dom/smil/crashtests/541297-1.svg b/dom/smil/crashtests/541297-1.svg new file mode 100644 index 000000000..4268232ba --- /dev/null +++ b/dom/smil/crashtests/541297-1.svg @@ -0,0 +1,22 @@ + + diff --git a/dom/smil/crashtests/547333-1.svg b/dom/smil/crashtests/547333-1.svg new file mode 100644 index 000000000..bac629b49 --- /dev/null +++ b/dom/smil/crashtests/547333-1.svg @@ -0,0 +1,22 @@ + + + + +abc + + diff --git a/dom/smil/crashtests/548899-1.svg b/dom/smil/crashtests/548899-1.svg new file mode 100644 index 000000000..c12ed2745 --- /dev/null +++ b/dom/smil/crashtests/548899-1.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/dom/smil/crashtests/551620-1.svg b/dom/smil/crashtests/551620-1.svg new file mode 100644 index 000000000..2ea83e9c2 --- /dev/null +++ b/dom/smil/crashtests/551620-1.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/dom/smil/crashtests/554141-1.svg b/dom/smil/crashtests/554141-1.svg new file mode 100644 index 000000000..61ce419f5 --- /dev/null +++ b/dom/smil/crashtests/554141-1.svg @@ -0,0 +1,12 @@ + + + + + + diff --git a/dom/smil/crashtests/554202-1.svg b/dom/smil/crashtests/554202-1.svg new file mode 100644 index 000000000..f3d692ca0 --- /dev/null +++ b/dom/smil/crashtests/554202-1.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + diff --git a/dom/smil/crashtests/554202-2.svg b/dom/smil/crashtests/554202-2.svg new file mode 100644 index 000000000..a3bbb3195 --- /dev/null +++ b/dom/smil/crashtests/554202-2.svg @@ -0,0 +1,19 @@ + + + + + + + + diff --git a/dom/smil/crashtests/555026-1.svg b/dom/smil/crashtests/555026-1.svg new file mode 100644 index 000000000..76b4cf075 --- /dev/null +++ b/dom/smil/crashtests/555026-1.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + diff --git a/dom/smil/crashtests/556841-1.svg b/dom/smil/crashtests/556841-1.svg new file mode 100644 index 000000000..92712deaa --- /dev/null +++ b/dom/smil/crashtests/556841-1.svg @@ -0,0 +1,16 @@ + + + + + + + diff --git a/dom/smil/crashtests/572938-1.svg b/dom/smil/crashtests/572938-1.svg new file mode 100644 index 000000000..d759944c7 --- /dev/null +++ b/dom/smil/crashtests/572938-1.svg @@ -0,0 +1,12 @@ + + + Used Text Element + + + + + Normal Text Element + + + diff --git a/dom/smil/crashtests/572938-2.svg b/dom/smil/crashtests/572938-2.svg new file mode 100644 index 000000000..8b9cf7b70 --- /dev/null +++ b/dom/smil/crashtests/572938-2.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + diff --git a/dom/smil/crashtests/572938-3.svg b/dom/smil/crashtests/572938-3.svg new file mode 100644 index 000000000..642ad32fb --- /dev/null +++ b/dom/smil/crashtests/572938-3.svg @@ -0,0 +1,10 @@ + + + Text A + Text B + + + + + diff --git a/dom/smil/crashtests/572938-4.svg b/dom/smil/crashtests/572938-4.svg new file mode 100644 index 000000000..549d43dd6 --- /dev/null +++ b/dom/smil/crashtests/572938-4.svg @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/dom/smil/crashtests/588287-1.svg b/dom/smil/crashtests/588287-1.svg new file mode 100644 index 000000000..cc35cf6b4 --- /dev/null +++ b/dom/smil/crashtests/588287-1.svg @@ -0,0 +1,24 @@ + + + + diff --git a/dom/smil/crashtests/588287-2.svg b/dom/smil/crashtests/588287-2.svg new file mode 100644 index 000000000..70d8e7639 --- /dev/null +++ b/dom/smil/crashtests/588287-2.svg @@ -0,0 +1,26 @@ + + + + diff --git a/dom/smil/crashtests/590425-1.html b/dom/smil/crashtests/590425-1.html new file mode 100644 index 000000000..3cd4805d9 --- /dev/null +++ b/dom/smil/crashtests/590425-1.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + diff --git a/dom/smil/crashtests/592477-1.xhtml b/dom/smil/crashtests/592477-1.xhtml new file mode 100644 index 000000000..c44a470a3 --- /dev/null +++ b/dom/smil/crashtests/592477-1.xhtml @@ -0,0 +1,26 @@ + + // + + + + + + + + + + + + + + + + diff --git a/dom/smil/crashtests/594653-1.svg b/dom/smil/crashtests/594653-1.svg new file mode 100644 index 000000000..76352ce30 --- /dev/null +++ b/dom/smil/crashtests/594653-1.svg @@ -0,0 +1,26 @@ + + + + + + + + + diff --git a/dom/smil/crashtests/596796-1.svg b/dom/smil/crashtests/596796-1.svg new file mode 100644 index 000000000..52a66fd58 --- /dev/null +++ b/dom/smil/crashtests/596796-1.svg @@ -0,0 +1,15 @@ + + + + + + + diff --git a/dom/smil/crashtests/605345-1.svg b/dom/smil/crashtests/605345-1.svg new file mode 100644 index 000000000..94887cf71 --- /dev/null +++ b/dom/smil/crashtests/605345-1.svg @@ -0,0 +1,25 @@ + + + + + diff --git a/dom/smil/crashtests/606101-1.svg b/dom/smil/crashtests/606101-1.svg new file mode 100644 index 000000000..988c86fa3 --- /dev/null +++ b/dom/smil/crashtests/606101-1.svg @@ -0,0 +1,23 @@ + + + diff --git a/dom/smil/crashtests/608295-1.html b/dom/smil/crashtests/608295-1.html new file mode 100644 index 000000000..354e6f909 --- /dev/null +++ b/dom/smil/crashtests/608295-1.html @@ -0,0 +1,18 @@ + + + + + + + + + diff --git a/dom/smil/crashtests/608549-1.svg b/dom/smil/crashtests/608549-1.svg new file mode 100644 index 000000000..dd441e013 --- /dev/null +++ b/dom/smil/crashtests/608549-1.svg @@ -0,0 +1,29 @@ + + + + + + + diff --git a/dom/smil/crashtests/611927-1.svg b/dom/smil/crashtests/611927-1.svg new file mode 100644 index 000000000..ea60f4ce1 --- /dev/null +++ b/dom/smil/crashtests/611927-1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dom/smil/crashtests/615002-1.svg b/dom/smil/crashtests/615002-1.svg new file mode 100644 index 000000000..eb9a29319 --- /dev/null +++ b/dom/smil/crashtests/615002-1.svg @@ -0,0 +1,16 @@ + + + + diff --git a/dom/smil/crashtests/615872-1.svg b/dom/smil/crashtests/615872-1.svg new file mode 100644 index 000000000..e0cdf2154 --- /dev/null +++ b/dom/smil/crashtests/615872-1.svg @@ -0,0 +1,21 @@ + + + diff --git a/dom/smil/crashtests/641388-1.html b/dom/smil/crashtests/641388-1.html new file mode 100644 index 000000000..4d1f0ea04 --- /dev/null +++ b/dom/smil/crashtests/641388-1.html @@ -0,0 +1,98 @@ + + +
+ diff --git a/dom/smil/crashtests/641388-2.html b/dom/smil/crashtests/641388-2.html new file mode 100644 index 000000000..9790e0f54 --- /dev/null +++ b/dom/smil/crashtests/641388-2.html @@ -0,0 +1,79 @@ + + diff --git a/dom/smil/crashtests/650732-1.svg b/dom/smil/crashtests/650732-1.svg new file mode 100644 index 000000000..95be31c16 --- /dev/null +++ b/dom/smil/crashtests/650732-1.svg @@ -0,0 +1,46 @@ + + + + + + + + diff --git a/dom/smil/crashtests/665334-1.svg b/dom/smil/crashtests/665334-1.svg new file mode 100644 index 000000000..94916d1e0 --- /dev/null +++ b/dom/smil/crashtests/665334-1.svg @@ -0,0 +1,13 @@ + + + + + diff --git a/dom/smil/crashtests/669225-1.svg b/dom/smil/crashtests/669225-1.svg new file mode 100644 index 000000000..966010563 --- /dev/null +++ b/dom/smil/crashtests/669225-1.svg @@ -0,0 +1,21 @@ + + + + + + + + + + diff --git a/dom/smil/crashtests/669225-2.svg b/dom/smil/crashtests/669225-2.svg new file mode 100644 index 000000000..00d52c1f4 --- /dev/null +++ b/dom/smil/crashtests/669225-2.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/dom/smil/crashtests/670313-1.svg b/dom/smil/crashtests/670313-1.svg new file mode 100644 index 000000000..97e12f35a --- /dev/null +++ b/dom/smil/crashtests/670313-1.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/dom/smil/crashtests/678822-1.svg b/dom/smil/crashtests/678822-1.svg new file mode 100644 index 000000000..a5e81ee10 --- /dev/null +++ b/dom/smil/crashtests/678822-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/678847-1.svg b/dom/smil/crashtests/678847-1.svg new file mode 100644 index 000000000..1fa2718cb --- /dev/null +++ b/dom/smil/crashtests/678847-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/678938-1.svg b/dom/smil/crashtests/678938-1.svg new file mode 100644 index 000000000..f3f8308fa --- /dev/null +++ b/dom/smil/crashtests/678938-1.svg @@ -0,0 +1,11 @@ + + + + diff --git a/dom/smil/crashtests/690994-1.svg b/dom/smil/crashtests/690994-1.svg new file mode 100644 index 000000000..252fd2c26 --- /dev/null +++ b/dom/smil/crashtests/690994-1.svg @@ -0,0 +1,17 @@ + + + + + + diff --git a/dom/smil/crashtests/691337-1.svg b/dom/smil/crashtests/691337-1.svg new file mode 100644 index 000000000..c341faa6b --- /dev/null +++ b/dom/smil/crashtests/691337-1.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/dom/smil/crashtests/691337-2.svg b/dom/smil/crashtests/691337-2.svg new file mode 100644 index 000000000..f4408ae5e --- /dev/null +++ b/dom/smil/crashtests/691337-2.svg @@ -0,0 +1,11 @@ + + + + + + + diff --git a/dom/smil/crashtests/697640-1.svg b/dom/smil/crashtests/697640-1.svg new file mode 100644 index 000000000..c2e1b89fd --- /dev/null +++ b/dom/smil/crashtests/697640-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/699325-1.svg b/dom/smil/crashtests/699325-1.svg new file mode 100644 index 000000000..7496c6ae2 --- /dev/null +++ b/dom/smil/crashtests/699325-1.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/dom/smil/crashtests/709907-1.svg b/dom/smil/crashtests/709907-1.svg new file mode 100644 index 000000000..631911970 --- /dev/null +++ b/dom/smil/crashtests/709907-1.svg @@ -0,0 +1,3 @@ + + + diff --git a/dom/smil/crashtests/720103-1.svg b/dom/smil/crashtests/720103-1.svg new file mode 100644 index 000000000..a51a3bf0f --- /dev/null +++ b/dom/smil/crashtests/720103-1.svg @@ -0,0 +1,4 @@ + + + + diff --git a/dom/smil/crashtests/crashtests.list b/dom/smil/crashtests/crashtests.list new file mode 100644 index 000000000..f783bb49e --- /dev/null +++ b/dom/smil/crashtests/crashtests.list @@ -0,0 +1,54 @@ +load 483584-1.svg +load 483584-2.svg +load 523188-1.svg +load 525099-1.svg +load 526536-1.svg +load 526875-1.svg +load 526875-2.svg +load 529387-1.xhtml +load 531550-1.svg +load 537157-1.svg +load 541297-1.svg +load 547333-1.svg +load 548899-1.svg +load 551620-1.svg +load 554141-1.svg +load 554202-1.svg +load 554202-2.svg +load 555026-1.svg +load 556841-1.svg +load 572938-1.svg +load 572938-2.svg +load 572938-3.svg +load 572938-4.svg +load 588287-1.svg +load 588287-2.svg +load 590425-1.html +load 592477-1.xhtml +load 594653-1.svg +load 596796-1.svg +load 605345-1.svg +load 606101-1.svg +load 608295-1.html +load 608549-1.svg +load 611927-1.svg +load 615002-1.svg +load 615872-1.svg +load 641388-1.html +load 641388-2.html +load 650732-1.svg +load 665334-1.svg +load 669225-1.svg +load 669225-2.svg +load 670313-1.svg +load 678822-1.svg +load 678847-1.svg +load 678938-1.svg +load 690994-1.svg +load 691337-1.svg +load 691337-2.svg +load 697640-1.svg +load 699325-1.svg +load 709907-1.svg +load 720103-1.svg +load 1010681-1.svg diff --git a/dom/smil/moz.build b/dom/smil/moz.build new file mode 100644 index 000000000..5b37ce642 --- /dev/null +++ b/dom/smil/moz.build @@ -0,0 +1,72 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +MOCHITEST_MANIFESTS += ['test/mochitest.ini'] + +EXPORTS += [ + 'nsISMILAttr.h', + 'nsISMILType.h', + 'nsSMILAnimationController.h', + 'nsSMILAnimationFunction.h', + 'nsSMILCompositorTable.h', + 'nsSMILCSSProperty.h', + 'nsSMILInstanceTime.h', + 'nsSMILInterval.h', + 'nsSMILKeySpline.h', + 'nsSMILMappedAttribute.h', + 'nsSMILMilestone.h', + 'nsSMILNullType.h', + 'nsSMILRepeatCount.h', + 'nsSMILSetAnimationFunction.h', + 'nsSMILTargetIdentifier.h', + 'nsSMILTimeContainer.h', + 'nsSMILTimedElement.h', + 'nsSMILTimeValue.h', + 'nsSMILTimeValueSpec.h', + 'nsSMILTimeValueSpecParams.h', + 'nsSMILTypes.h', + 'nsSMILValue.h', +] + +EXPORTS.mozilla.dom += [ + 'TimeEvent.h', +] + +UNIFIED_SOURCES += [ + 'nsSMILAnimationController.cpp', + 'nsSMILAnimationFunction.cpp', + 'nsSMILCompositor.cpp', + 'nsSMILCSSProperty.cpp', + 'nsSMILCSSValueType.cpp', + 'nsSMILFloatType.cpp', + 'nsSMILInstanceTime.cpp', + 'nsSMILInterval.cpp', + 'nsSMILKeySpline.cpp', + 'nsSMILMappedAttribute.cpp', + 'nsSMILNullType.cpp', + 'nsSMILParserUtils.cpp', + 'nsSMILRepeatCount.cpp', + 'nsSMILSetAnimationFunction.cpp', + 'nsSMILTimeContainer.cpp', + 'nsSMILTimedElement.cpp', + 'nsSMILTimeValue.cpp', + 'nsSMILTimeValueSpec.cpp', + 'nsSMILValue.cpp', + 'SMILBoolType.cpp', + 'SMILEnumType.cpp', + 'SMILIntegerType.cpp', + 'SMILStringType.cpp', + 'TimeEvent.cpp', +] + +LOCAL_INCLUDES += [ + '/dom/base', + '/dom/svg', + '/layout/base', + '/layout/style', +] + +FINAL_LIBRARY = 'xul' diff --git a/dom/smil/nsISMILAttr.h b/dom/smil/nsISMILAttr.h new file mode 100644 index 000000000..de65a42bb --- /dev/null +++ b/dom/smil/nsISMILAttr.h @@ -0,0 +1,98 @@ +/* -*- 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/. */ + +#ifndef NS_ISMILATTR_H_ +#define NS_ISMILATTR_H_ + +#include "nscore.h" + +class nsSMILValue; +class nsIContent; +class nsAString; + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +//////////////////////////////////////////////////////////////////////// +// nsISMILAttr: A variable targeted by SMIL for animation and can therefore have +// an underlying (base) value and an animated value For example, an attribute of +// a particular SVG element. +// +// These objects only exist during the compositing phase of SMIL animation +// calculations. They have a single owner who is responsible for deleting the +// object. + +class nsISMILAttr +{ +public: + /** + * Creates a new nsSMILValue for this attribute from a string. The string is + * parsed in the context of this attribute so that context-dependent values + * such as em-based units can be resolved into a canonical form suitable for + * animation (including interpolation etc.). + * + * @param aStr A string defining the new value to be created. + * @param aSrcElement The source animation element. This may be needed to + * provided additional context data such as for + * animateTransform where the 'type' attribute is needed to + * parse the value. + * @param[out] aValue Outparam for storing the parsed value. + * @param[out] aPreventCachingOfSandwich + * Outparam to indicate whether the attribute contains + * dependencies on its context that should prevent the + * result of the animation sandwich from being cached and + * reused in future samples. + * @return NS_OK on success or an error code if creation failed. + */ + virtual nsresult ValueFromString(const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const = 0; + + /** + * Gets the underlying value of this attribute. + * + * @return an nsSMILValue object. returned_object.IsNull() will be true if an + * error occurred. + */ + virtual nsSMILValue GetBaseValue() const = 0; + + /** + * Clears the animated value of this attribute. + * + * NOTE: The animation target is not guaranteed to be in a document when this + * method is called. (See bug 523188) + */ + virtual void ClearAnimValue() = 0; + + /** + * Sets the presentation value of this attribute. + * + * @param aValue The value to set. + * @return NS_OK on success or an error code if setting failed. + */ + virtual nsresult SetAnimValue(const nsSMILValue& aValue) = 0; + + /** + * Returns the targeted content node, for any nsISMILAttr implementations + * that want to expose that to the animation logic. Otherwise, returns + * null. + * + * @return the targeted content node, if this nsISMILAttr implementation + * wishes to make it avaiable. Otherwise, nullptr. + */ + virtual const nsIContent* GetTargetNode() const { return nullptr; } + + /** + * Virtual destructor, to make sure subclasses can clean themselves up. + */ + virtual ~nsISMILAttr() {} +}; + +#endif // NS_ISMILATTR_H_ diff --git a/dom/smil/nsISMILType.h b/dom/smil/nsISMILType.h new file mode 100644 index 000000000..f923a9c77 --- /dev/null +++ b/dom/smil/nsISMILType.h @@ -0,0 +1,214 @@ +/* -*- 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/. */ + +#ifndef NS_ISMILTYPE_H_ +#define NS_ISMILTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nscore.h" + +class nsSMILValue; + +////////////////////////////////////////////////////////////////////////////// +// nsISMILType: Interface for defining the basic operations needed for animating +// a particular kind of data (e.g. lengths, colors, transformation matrices). +// +// This interface is never used directly but always through an nsSMILValue that +// bundles together a pointer to a concrete implementation of this interface and +// the data upon which it should operate. +// +// We keep the data and type separate rather than just providing different +// subclasses of nsSMILValue. This is so that sizeof(nsSMILValue) is the same +// for all value types, allowing us to have a type-agnostic nsTArray of +// nsSMILValue objects (actual objects, not pointers). It also allows most +// nsSMILValues (except those that need to allocate extra memory for their +// data) to be allocated on the stack and directly assigned to one another +// provided performance benefits for the animation code. +// +// Note that different types have different capabilities. Roughly speaking there +// are probably three main types: +// +// +---------------------+---------------+-------------+------------------+ +// | CATEGORY: | DISCRETE | LINEAR | ADDITIVE | +// +---------------------+---------------+-------------+------------------+ +// | Example: | strings, | path data? | lengths, | +// | | color k/words?| | RGB color values | +// | | | | | +// | -- Assign? | X | X | X | +// | -- Add? | - | X? | X | +// | -- SandwichAdd? | - | -? | X | +// | -- ComputeDistance? | - | - | X? | +// | -- Interpolate? | - | X | X | +// +---------------------+---------------+-------------+------------------+ +// + +class nsISMILType +{ + /** + * Only give the nsSMILValue class access to this interface. + */ + friend class nsSMILValue; + +protected: + /** + * Initialises aValue and sets it to some identity value such that adding + * aValue to another value of the same type has no effect. + * + * @pre aValue.IsNull() + * @post aValue.mType == this + */ + virtual void Init(nsSMILValue& aValue) const = 0; + + /** + * Destroys any data associated with a value of this type. + * + * @pre aValue.mType == this + * @post aValue.IsNull() + */ + virtual void Destroy(nsSMILValue& aValue) const = 0; + + /** + * Assign this object the value of another. Think of this as the assignment + * operator. + * + * @param aDest The left-hand side of the assignment. + * @param aSrc The right-hand side of the assignment. + * @return NS_OK on success, an error code on failure such as when the + * underlying type of the specified object differs. + * + * @pre aDest.mType == aSrc.mType == this + */ + virtual nsresult Assign(nsSMILValue& aDest, + const nsSMILValue& aSrc) const = 0; + + /** + * Test two nsSMILValue objects (of this nsISMILType) for equality. + * + * A return value of true represents a guarantee that aLeft and aRight are + * equal. (That is, they would behave identically if passed to the methods + * Add, SandwichAdd, ComputeDistance, and Interpolate). + * + * A return value of false simply indicates that we make no guarantee + * about equality. + * + * NOTE: It's perfectly legal for implementations of this method to return + * false in all cases. However, smarter implementations will make this + * method more useful for optimization. + * + * @param aLeft The left-hand side of the equality check. + * @param aRight The right-hand side of the equality check. + * @return true if we're sure the values are equal, false otherwise. + * + * @pre aDest.mType == aSrc.mType == this + */ + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const = 0; + + /** + * Adds two values. + * + * The count parameter facilitates repetition. + * + * By equation, + * + * aDest += aValueToAdd * aCount + * + * Therefore, if aCount == 0, aDest will be unaltered. + * + * This method will fail if this data type is not additive or the value was + * not specified using an additive syntax. + * + * See SVG 1.1, section 19.2.5. In particular, + * + * "If a given attribute or property can take values of keywords (which are + * not additive) or numeric values (which are additive), then additive + * animations are possible if the subsequent animation uses a numeric value + * even if the base animation uses a keyword value; however, if the + * subsequent animation uses a keyword value, additive animation is not + * possible." + * + * If this method fails (e.g. because the data type is not additive), aDest + * will be unaltered. + * + * @param aDest The value to add to. + * @param aValueToAdd The value to add. + * @param aCount The number of times to add aValueToAdd. + * @return NS_OK on success, an error code on failure. + * + * @pre aValueToAdd.mType == aDest.mType == this + */ + virtual nsresult Add(nsSMILValue& aDest, + const nsSMILValue& aValueToAdd, + uint32_t aCount) const = 0; + + /** + * Adds aValueToAdd to the underlying value in the animation sandwich, aDest. + * + * For most types this operation is identical to a regular Add() but for some + * types (notably ) the operation differs. For + * Add() corresponds to simply adding together the + * transform parameters and is used when calculating cumulative values or + * by-animation values. On the other hand SandwichAdd() is used when adding to + * the underlying value and requires matrix post-multiplication. (This + * distinction is most clearly indicated by the SVGT1.2 test suite. It is not + * obvious within the SMIL specifications.) + * + * @param aDest The value to add to. + * @param aValueToAdd The value to add. + * @return NS_OK on success, an error code on failure. + * + * @pre aValueToAdd.mType == aDest.mType == this + */ + virtual nsresult SandwichAdd(nsSMILValue& aDest, + const nsSMILValue& aValueToAdd) const + { + return Add(aDest, aValueToAdd, 1); + } + + /** + * Calculates the 'distance' between two values. This is the distance used in + * paced interpolation. + * + * @param aFrom The start of the interval for which the distance should + * be calculated. + * @param aTo The end of the interval for which the distance should be + * calculated. + * @param aDistance The result of the calculation. + * @return NS_OK on success, or an appropriate error code if there is no + * notion of distance for the underlying data type or the distance + * could not be calculated. + * + * @pre aFrom.mType == aTo.mType == this + */ + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const = 0; + + /** + * Calculates an interpolated value between two values using the specified + * proportion. + * + * @param aStartVal The value defining the start of the interval of + * interpolation. + * @param aEndVal The value defining the end of the interval of + * interpolation. + * @param aUnitDistance A number between 0.0 and 1.0 (inclusive) defining + * the distance of the interpolated value in the + * interval. + * @param aResult The interpolated value. + * @return NS_OK on success, NS_ERROR_FAILURE if this data type cannot be + * interpolated or NS_ERROR_OUT_OF_MEMORY if insufficient memory was + * available for storing the result. + * + * @pre aStartVal.mType == aEndVal.mType == aResult.mType == this + */ + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const = 0; +}; + +#endif // NS_ISMILTYPE_H_ diff --git a/dom/smil/nsSMILAnimationController.cpp b/dom/smil/nsSMILAnimationController.cpp new file mode 100644 index 000000000..0dd616346 --- /dev/null +++ b/dom/smil/nsSMILAnimationController.cpp @@ -0,0 +1,795 @@ +/* -*- 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 "nsSMILAnimationController.h" +#include "nsSMILCompositor.h" +#include "nsSMILCSSProperty.h" +#include "nsCSSProps.h" +#include "nsITimer.h" +#include "mozilla/dom/Element.h" +#include "nsIDocument.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsSMILTimedElement.h" +#include +#include "mozilla/AutoRestore.h" +#include "RestyleTracker.h" + +using namespace mozilla; +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// nsSMILAnimationController implementation + +//---------------------------------------------------------------------- +// ctors, dtors, factory methods + +nsSMILAnimationController::nsSMILAnimationController(nsIDocument* aDoc) + : mAvgTimeBetweenSamples(0), + mResampleNeeded(false), + mDeferredStartSampling(false), + mRunningSample(false), + mRegisteredWithRefreshDriver(false), + mMightHavePendingStyleUpdates(false), + mDocument(aDoc) +{ + MOZ_ASSERT(aDoc, "need a non-null document"); + + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + if (refreshDriver) { + mStartTime = refreshDriver->MostRecentRefresh(); + } else { + mStartTime = mozilla::TimeStamp::Now(); + } + mCurrentSampleTime = mStartTime; + + Begin(); +} + +nsSMILAnimationController::~nsSMILAnimationController() +{ + NS_ASSERTION(mAnimationElementTable.Count() == 0, + "Animation controller shouldn't be tracking any animation" + " elements when it dies"); + NS_ASSERTION(!mRegisteredWithRefreshDriver, + "Leaving stale entry in refresh driver's observer list"); +} + +void +nsSMILAnimationController::Disconnect() +{ + MOZ_ASSERT(mDocument, "disconnecting when we weren't connected...?"); + MOZ_ASSERT(mRefCnt.get() == 1, + "Expecting to disconnect when doc is sole remaining owner"); + NS_ASSERTION(mPauseState & nsSMILTimeContainer::PAUSE_PAGEHIDE, + "Expecting to be paused for pagehide before disconnect"); + + StopSampling(GetRefreshDriver()); + + mDocument = nullptr; // (raw pointer) +} + +//---------------------------------------------------------------------- +// nsSMILTimeContainer methods: + +void +nsSMILAnimationController::Pause(uint32_t aType) +{ + nsSMILTimeContainer::Pause(aType); + + if (mPauseState) { + mDeferredStartSampling = false; + StopSampling(GetRefreshDriver()); + } +} + +void +nsSMILAnimationController::Resume(uint32_t aType) +{ + bool wasPaused = (mPauseState != 0); + // Update mCurrentSampleTime so that calls to GetParentTime--used for + // calculating parent offsets--are accurate + mCurrentSampleTime = mozilla::TimeStamp::Now(); + + nsSMILTimeContainer::Resume(aType); + + if (wasPaused && !mPauseState && mChildContainerTable.Count()) { + MaybeStartSampling(GetRefreshDriver()); + Sample(); // Run the first sample manually + } +} + +nsSMILTime +nsSMILAnimationController::GetParentTime() const +{ + return (nsSMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds(); +} + +//---------------------------------------------------------------------- +// nsARefreshObserver methods: +NS_IMPL_ADDREF(nsSMILAnimationController) +NS_IMPL_RELEASE(nsSMILAnimationController) + +// nsRefreshDriver Callback function +void +nsSMILAnimationController::WillRefresh(mozilla::TimeStamp aTime) +{ + // Although we never expect aTime to go backwards, when we initialise the + // animation controller, if we can't get hold of a refresh driver we + // initialise mCurrentSampleTime to Now(). It may be possible that after + // doing so we get sampled by a refresh driver whose most recent refresh time + // predates when we were initialised, so to be safe we make sure to take the + // most recent time here. + aTime = std::max(mCurrentSampleTime, aTime); + + // Sleep detection: If the time between samples is a whole lot greater than we + // were expecting then we assume the computer went to sleep or someone's + // messing with the clock. In that case, fiddle our parent offset and use our + // average time between samples to calculate the new sample time. This + // prevents us from hanging while trying to catch up on all the missed time. + + // Smoothing of coefficient for the average function. 0.2 should let us track + // the sample rate reasonably tightly without being overly affected by + // occasional delays. + static const double SAMPLE_DUR_WEIGHTING = 0.2; + // If the elapsed time exceeds our expectation by this number of times we'll + // initiate special behaviour to basically ignore the intervening time. + static const double SAMPLE_DEV_THRESHOLD = 200.0; + + nsSMILTime elapsedTime = + (nsSMILTime)(aTime - mCurrentSampleTime).ToMilliseconds(); + if (mAvgTimeBetweenSamples == 0) { + // First sample. + mAvgTimeBetweenSamples = elapsedTime; + } else { + if (elapsedTime > SAMPLE_DEV_THRESHOLD * mAvgTimeBetweenSamples) { + // Unexpectedly long delay between samples. + NS_WARNING("Detected really long delay between samples, continuing from " + "previous sample"); + mParentOffset += elapsedTime - mAvgTimeBetweenSamples; + } + // Update the moving average. Due to truncation here the average will + // normally be a little less than it should be but that's probably ok. + mAvgTimeBetweenSamples = + (nsSMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING + + mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING)); + } + mCurrentSampleTime = aTime; + + Sample(); +} + +//---------------------------------------------------------------------- +// Animation element registration methods: + +void +nsSMILAnimationController::RegisterAnimationElement( + SVGAnimationElement* aAnimationElement) +{ + mAnimationElementTable.PutEntry(aAnimationElement); + if (mDeferredStartSampling) { + mDeferredStartSampling = false; + if (mChildContainerTable.Count()) { + // mAnimationElementTable was empty, but now we've added its 1st element + MOZ_ASSERT(mAnimationElementTable.Count() == 1, + "we shouldn't have deferred sampling if we already had " + "animations registered"); + StartSampling(GetRefreshDriver()); + Sample(); // Run the first sample manually + } // else, don't sample until a time container is registered (via AddChild) + } +} + +void +nsSMILAnimationController::UnregisterAnimationElement( + SVGAnimationElement* aAnimationElement) +{ + mAnimationElementTable.RemoveEntry(aAnimationElement); +} + +//---------------------------------------------------------------------- +// Page show/hide + +void +nsSMILAnimationController::OnPageShow() +{ + Resume(nsSMILTimeContainer::PAUSE_PAGEHIDE); +} + +void +nsSMILAnimationController::OnPageHide() +{ + Pause(nsSMILTimeContainer::PAUSE_PAGEHIDE); +} + +//---------------------------------------------------------------------- +// Cycle-collection support + +void +nsSMILAnimationController::Traverse( + nsCycleCollectionTraversalCallback* aCallback) +{ + // Traverse last compositor table + if (mLastCompositorTable) { + for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { + nsSMILCompositor* compositor = iter.Get(); + compositor->Traverse(aCallback); + } + } +} + +void +nsSMILAnimationController::Unlink() +{ + mLastCompositorTable = nullptr; +} + +//---------------------------------------------------------------------- +// Refresh driver lifecycle related methods + +void +nsSMILAnimationController::NotifyRefreshDriverCreated( + nsRefreshDriver* aRefreshDriver) +{ + if (!mPauseState) { + MaybeStartSampling(aRefreshDriver); + } +} + +void +nsSMILAnimationController::NotifyRefreshDriverDestroying( + nsRefreshDriver* aRefreshDriver) +{ + if (!mPauseState && !mDeferredStartSampling) { + StopSampling(aRefreshDriver); + } +} + +//---------------------------------------------------------------------- +// Timer-related implementation helpers + +void +nsSMILAnimationController::StartSampling(nsRefreshDriver* aRefreshDriver) +{ + NS_ASSERTION(mPauseState == 0, "Starting sampling but controller is paused"); + NS_ASSERTION(!mDeferredStartSampling, + "Started sampling but the deferred start flag is still set"); + if (aRefreshDriver) { + MOZ_ASSERT(!mRegisteredWithRefreshDriver, + "Redundantly registering with refresh driver"); + MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(), + "Starting sampling with wrong refresh driver"); + // We're effectively resuming from a pause so update our current sample time + // or else it will confuse our "average time between samples" calculations. + mCurrentSampleTime = mozilla::TimeStamp::Now(); + aRefreshDriver->AddRefreshObserver(this, Flush_Style); + mRegisteredWithRefreshDriver = true; + } +} + +void +nsSMILAnimationController::StopSampling(nsRefreshDriver* aRefreshDriver) +{ + if (aRefreshDriver && mRegisteredWithRefreshDriver) { + // NOTE: The document might already have been detached from its PresContext + // (and RefreshDriver), which would make GetRefreshDriver() return null. + MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(), + "Stopping sampling with wrong refresh driver"); + aRefreshDriver->RemoveRefreshObserver(this, Flush_Style); + mRegisteredWithRefreshDriver = false; + } +} + +void +nsSMILAnimationController::MaybeStartSampling(nsRefreshDriver* aRefreshDriver) +{ + if (mDeferredStartSampling) { + // We've received earlier 'MaybeStartSampling' calls, and we're + // deferring until we get a registered animation. + return; + } + + if (mAnimationElementTable.Count()) { + StartSampling(aRefreshDriver); + } else { + mDeferredStartSampling = true; + } +} + +//---------------------------------------------------------------------- +// Sample-related methods and callbacks + +void +nsSMILAnimationController::DoSample() +{ + DoSample(true); // Skip unchanged time containers +} + +void +nsSMILAnimationController::DoSample(bool aSkipUnchangedContainers) +{ + if (!mDocument) { + NS_ERROR("Shouldn't be sampling after document has disconnected"); + return; + } + if (mRunningSample) { + NS_ERROR("Shouldn't be recursively sampling"); + return; + } + + bool isStyleFlushNeeded = mResampleNeeded; + mResampleNeeded = false; + nsCOMPtr document(mDocument); // keeps 'this' alive too + + // Set running sample flag -- do this before flushing styles so that when we + // flush styles we don't end up requesting extra samples + AutoRestore autoRestoreRunningSample(mRunningSample); + mRunningSample = true; + + // STEP 1: Bring model up to date + // (i) Rewind elements where necessary + // (ii) Run milestone samples + RewindElements(); + DoMilestoneSamples(); + + // STEP 2: Sample the child time containers + // + // When we sample the child time containers they will simply record the sample + // time in document time. + TimeContainerHashtable activeContainers(mChildContainerTable.Count()); + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + nsSMILTimeContainer* container = iter.Get()->GetKey(); + if (!container) { + continue; + } + + if (!container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN) && + (container->NeedsSample() || !aSkipUnchangedContainers)) { + container->ClearMilestones(); + container->Sample(); + container->MarkSeekFinished(); + activeContainers.PutEntry(container); + } + } + + // STEP 3: (i) Sample the timed elements AND + // (ii) Create a table of compositors + // + // (i) Here we sample the timed elements (fetched from the + // SVGAnimationElements) which determine from the active time if the + // element is active and what its simple time etc. is. This information is + // then passed to its time client (nsSMILAnimationFunction). + // + // (ii) During the same loop we also build up a table that contains one + // compositor for each animated attribute and which maps animated elements to + // the corresponding compositor for their target attribute. + // + // Note that this compositor table needs to be allocated on the heap so we can + // store it until the next sample. This lets us find out which elements were + // animated in sample 'n-1' but not in sample 'n' (and hence need to have + // their animation effects removed in sample 'n'). + // + // Parts (i) and (ii) are not functionally related but we combine them here to + // save iterating over the animation elements twice. + + // Create the compositor table + nsAutoPtr + currentCompositorTable(new nsSMILCompositorTable(0)); + nsTArray> + animElems(mAnimationElementTable.Count()); + + for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) { + SVGAnimationElement* animElem = iter.Get()->GetKey(); + SampleTimedElement(animElem, &activeContainers); + AddAnimationToCompositorTable(animElem, + currentCompositorTable, + isStyleFlushNeeded); + animElems.AppendElement(animElem); + } + activeContainers.Clear(); + + // STEP 4: Compare previous sample's compositors against this sample's. + // (Transfer cached base values across, & remove animation effects from + // no-longer-animated targets.) + if (mLastCompositorTable) { + // * Transfer over cached base values, from last sample's compositors + for (auto iter = currentCompositorTable->Iter(); + !iter.Done(); + iter.Next()) { + nsSMILCompositor* compositor = iter.Get(); + nsSMILCompositor* lastCompositor = + mLastCompositorTable->GetEntry(compositor->GetKey()); + + if (lastCompositor) { + compositor->StealCachedBaseValue(lastCompositor); + } + } + + // * For each compositor in current sample's hash table, remove entry from + // prev sample's hash table -- we don't need to clear animation + // effects of those compositors, since they're still being animated. + for (auto iter = currentCompositorTable->Iter(); + !iter.Done(); + iter.Next()) { + mLastCompositorTable->RemoveEntry(iter.Get()->GetKey()); + } + + // * For each entry that remains in prev sample's hash table (i.e. for + // every target that's no longer animated), clear animation effects. + for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { + iter.Get()->ClearAnimationEffects(); + } + } + + // return early if there are no active animations to avoid a style flush + if (currentCompositorTable->Count() == 0) { + mLastCompositorTable = nullptr; + return; + } + + if (isStyleFlushNeeded) { + document->FlushPendingNotifications(Flush_Style); + } + + // WARNING: + // WARNING: the above flush may have destroyed the pres shell and/or + // WARNING: frames and other layout related objects. + // WARNING: + + // STEP 5: Compose currently-animated attributes. + // XXXdholbert: This step traverses our animation targets in an effectively + // random order. For animation from/to 'inherit' values to work correctly + // when the inherited value is *also* being animated, we really should be + // traversing our animated nodes in an ancestors-first order (bug 501183) + bool mightHavePendingStyleUpdates = false; + for (auto iter = currentCompositorTable->Iter(); !iter.Done(); iter.Next()) { + iter.Get()->ComposeAttribute(mightHavePendingStyleUpdates); + } + + // Update last compositor table + mLastCompositorTable = currentCompositorTable.forget(); + mMightHavePendingStyleUpdates = mightHavePendingStyleUpdates; + + NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!"); +} + +void +nsSMILAnimationController::RewindElements() +{ + bool rewindNeeded = false; + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + nsSMILTimeContainer* container = iter.Get()->GetKey(); + if (container->NeedsRewind()) { + rewindNeeded = true; + break; + } + } + + if (!rewindNeeded) + return; + + for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) { + SVGAnimationElement* animElem = iter.Get()->GetKey(); + nsSMILTimeContainer* timeContainer = animElem->GetTimeContainer(); + if (timeContainer && timeContainer->NeedsRewind()) { + animElem->TimedElement().Rewind(); + } + } + + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + iter.Get()->GetKey()->ClearNeedsRewind(); + } +} + +void +nsSMILAnimationController::DoMilestoneSamples() +{ + // We need to sample the timing model but because SMIL operates independently + // of the frame-rate, we can get one sample at t=0s and the next at t=10min. + // + // In between those two sample times a whole string of significant events + // might be expected to take place: events firing, new interdependencies + // between animations resolved and dissolved, etc. + // + // Furthermore, at any given time, we want to sample all the intervals that + // end at that time BEFORE any that begin. This behaviour is implied by SMIL's + // endpoint-exclusive timing model. + // + // So we have the animations (specifically the timed elements) register the + // next significant moment (called a milestone) in their lifetime and then we + // step through the model at each of these moments and sample those animations + // registered for those times. This way events can fire in the correct order, + // dependencies can be resolved etc. + + nsSMILTime sampleTime = INT64_MIN; + + while (true) { + // We want to find any milestones AT OR BEFORE the current sample time so we + // initialise the next milestone to the moment after (1ms after, to be + // precise) the current sample time and see if there are any milestones + // before that. Any other milestones will be dealt with in a subsequent + // sample. + nsSMILMilestone nextMilestone(GetCurrentTime() + 1, true); + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + nsSMILTimeContainer* container = iter.Get()->GetKey(); + if (container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) { + continue; + } + nsSMILMilestone thisMilestone; + bool didGetMilestone = + container->GetNextMilestoneInParentTime(thisMilestone); + if (didGetMilestone && thisMilestone < nextMilestone) { + nextMilestone = thisMilestone; + } + } + + if (nextMilestone.mTime > GetCurrentTime()) { + break; + } + + nsTArray> elements; + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + nsSMILTimeContainer* container = iter.Get()->GetKey(); + if (container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) { + continue; + } + container->PopMilestoneElementsAtMilestone(nextMilestone, elements); + } + + uint32_t length = elements.Length(); + + // During the course of a sampling we don't want to actually go backwards. + // Due to negative offsets, early ends and the like, a timed element might + // register a milestone that is actually in the past. That's fine, but it's + // still only going to get *sampled* with whatever time we're up to and no + // earlier. + // + // Because we're only performing this clamping at the last moment, the + // animations will still all get sampled in the correct order and + // dependencies will be appropriately resolved. + sampleTime = std::max(nextMilestone.mTime, sampleTime); + + for (uint32_t i = 0; i < length; ++i) { + SVGAnimationElement* elem = elements[i].get(); + MOZ_ASSERT(elem, "nullptr animation element in list"); + nsSMILTimeContainer* container = elem->GetTimeContainer(); + if (!container) + // The container may be nullptr if the element has been detached from its + // parent since registering a milestone. + continue; + + nsSMILTimeValue containerTimeValue = + container->ParentToContainerTime(sampleTime); + if (!containerTimeValue.IsDefinite()) + continue; + + // Clamp the converted container time to non-negative values. + nsSMILTime containerTime = std::max(0, containerTimeValue.GetMillis()); + + if (nextMilestone.mIsEnd) { + elem->TimedElement().SampleEndAt(containerTime); + } else { + elem->TimedElement().SampleAt(containerTime); + } + } + } +} + +/*static*/ void +nsSMILAnimationController::SampleTimedElement( + SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) +{ + nsSMILTimeContainer* timeContainer = aElement->GetTimeContainer(); + if (!timeContainer) + return; + + // We'd like to call timeContainer->NeedsSample() here and skip all timed + // elements that belong to paused time containers that don't need a sample, + // but that doesn't work because we've already called Sample() on all the time + // containers so the paused ones don't need a sample any more and they'll + // return false. + // + // Instead we build up a hashmap of active time containers during the previous + // step (SampleTimeContainer) and then test here if the container for this + // timed element is in the list. + if (!aActiveContainers->GetEntry(timeContainer)) + return; + + nsSMILTime containerTime = timeContainer->GetCurrentTime(); + + MOZ_ASSERT(!timeContainer->IsSeeking(), + "Doing a regular sample but the time container is still seeking"); + aElement->TimedElement().SampleAt(containerTime); +} + +/*static*/ void +nsSMILAnimationController::AddAnimationToCompositorTable( + SVGAnimationElement* aElement, + nsSMILCompositorTable* aCompositorTable, + bool& aStyleFlushNeeded) +{ + // Add a compositor to the hash table if there's not already one there + nsSMILTargetIdentifier key; + if (!GetTargetIdentifierForAnimation(aElement, key)) + // Something's wrong/missing about animation's target; skip this animation + return; + + nsSMILAnimationFunction& func = aElement->AnimationFunction(); + + // Only add active animation functions. If there are no active animations + // targeting an attribute, no compositor will be created and any previously + // applied animations will be cleared. + if (func.IsActiveOrFrozen()) { + // Look up the compositor for our target, & add our animation function + // to its list of animation functions. + nsSMILCompositor* result = aCompositorTable->PutEntry(key); + result->AddAnimationFunction(&func); + + } else if (func.HasChanged()) { + // Look up the compositor for our target, and force it to skip the + // "nothing's changed so don't bother compositing" optimization for this + // sample. |func| is inactive, but it's probably *newly* inactive (since + // it's got HasChanged() == true), so we need to make sure to recompose + // its target. + nsSMILCompositor* result = aCompositorTable->PutEntry(key); + result->ToggleForceCompositing(); + + // We've now made sure that |func|'s inactivity will be reflected as of + // this sample. We need to clear its HasChanged() flag so that it won't + // trigger this same clause in future samples (until it changes again). + func.ClearHasChanged(); + } + aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample(); +} + +static inline bool +IsTransformAttribute(int32_t aNamespaceID, nsIAtom *aAttributeName) +{ + return aNamespaceID == kNameSpaceID_None && + (aAttributeName == nsGkAtoms::transform || + aAttributeName == nsGkAtoms::patternTransform || + aAttributeName == nsGkAtoms::gradientTransform); +} + +// Helper function that, given a SVGAnimationElement, looks up its target +// element & target attribute and populates a nsSMILTargetIdentifier +// for this target. +/*static*/ bool +nsSMILAnimationController::GetTargetIdentifierForAnimation( + SVGAnimationElement* aAnimElem, nsSMILTargetIdentifier& aResult) +{ + // Look up target (animated) element + Element* targetElem = aAnimElem->GetTargetElementContent(); + if (!targetElem) + // Animation has no target elem -- skip it. + return false; + + // Look up target (animated) attribute + // SMILANIM section 3.1, attributeName may + // have an XMLNS prefix to indicate the XML namespace. + nsCOMPtr attributeName; + int32_t attributeNamespaceID; + if (!aAnimElem->GetTargetAttributeName(&attributeNamespaceID, + getter_AddRefs(attributeName))) + // Animation has no target attr -- skip it. + return false; + + // animateTransform can only animate transforms, conversely transforms + // can only be animated by animateTransform + if (IsTransformAttribute(attributeNamespaceID, attributeName) != + (aAnimElem->IsSVGElement(nsGkAtoms::animateTransform))) + return false; + + // Look up target (animated) attribute-type + nsSMILTargetAttrType attributeType = aAnimElem->GetTargetAttributeType(); + + // Check if an 'auto' attributeType refers to a CSS property or XML attribute. + // Note that SMIL requires we search for CSS properties first. So if they + // overlap, 'auto' = 'CSS'. (SMILANIM 3.1) + bool isCSS = false; + if (attributeType == eSMILTargetAttrType_auto) { + if (attributeNamespaceID == kNameSpaceID_None) { + // width/height are special as they may be attributes or for + // outer- elements, mapped into style. + if (attributeName == nsGkAtoms::width || + attributeName == nsGkAtoms::height) { + isCSS = targetElem->GetNameSpaceID() != kNameSpaceID_SVG; + } else { + nsCSSPropertyID prop = + nsCSSProps::LookupProperty(nsDependentAtomString(attributeName), + CSSEnabledState::eForAllContent); + isCSS = nsSMILCSSProperty::IsPropertyAnimatable(prop); + } + } + } else { + isCSS = (attributeType == eSMILTargetAttrType_CSS); + } + + // Construct the key + aResult.mElement = targetElem; + aResult.mAttributeName = attributeName; + aResult.mAttributeNamespaceID = attributeNamespaceID; + aResult.mIsCSS = isCSS; + + return true; +} + +void +nsSMILAnimationController::AddStyleUpdatesTo(RestyleTracker& aTracker) +{ + MOZ_ASSERT(mMightHavePendingStyleUpdates, + "Should only add style updates when we think we might have some"); + + for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) { + SVGAnimationElement* animElement = iter.Get()->GetKey(); + + nsSMILTargetIdentifier key; + if (!GetTargetIdentifierForAnimation(animElement, key)) { + // Something's wrong/missing about animation's target; skip this animation + continue; + } + + // mIsCSS true means that the rules are the ones returned from + // Element::GetSMILOverrideStyleDeclaration (via nsSMILCSSProperty objects), + // and mIsCSS false means the rules are nsSMILMappedAttribute objects + // returned from nsSVGElement::GetAnimatedContentStyleRule. + nsRestyleHint rshint = key.mIsCSS ? eRestyle_StyleAttribute_Animations + : eRestyle_SVGAttrAnimations; + aTracker.AddPendingRestyle(key.mElement, rshint, nsChangeHint(0)); + } + + mMightHavePendingStyleUpdates = false; +} + +//---------------------------------------------------------------------- +// Add/remove child time containers + +nsresult +nsSMILAnimationController::AddChild(nsSMILTimeContainer& aChild) +{ + TimeContainerPtrKey* key = mChildContainerTable.PutEntry(&aChild); + NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY); + + if (!mPauseState && mChildContainerTable.Count() == 1) { + MaybeStartSampling(GetRefreshDriver()); + Sample(); // Run the first sample manually + } + + return NS_OK; +} + +void +nsSMILAnimationController::RemoveChild(nsSMILTimeContainer& aChild) +{ + mChildContainerTable.RemoveEntry(&aChild); + + if (!mPauseState && mChildContainerTable.Count() == 0) { + StopSampling(GetRefreshDriver()); + } +} + +// Helper method +nsRefreshDriver* +nsSMILAnimationController::GetRefreshDriver() +{ + if (!mDocument) { + NS_ERROR("Requesting refresh driver after document has disconnected!"); + return nullptr; + } + + nsIPresShell* shell = mDocument->GetShell(); + if (!shell) { + return nullptr; + } + + nsPresContext* context = shell->GetPresContext(); + return context ? context->RefreshDriver() : nullptr; +} + +void +nsSMILAnimationController::FlagDocumentNeedsFlush() +{ + mDocument->SetNeedStyleFlush(); +} diff --git a/dom/smil/nsSMILAnimationController.h b/dom/smil/nsSMILAnimationController.h new file mode 100644 index 000000000..9c565b78b --- /dev/null +++ b/dom/smil/nsSMILAnimationController.h @@ -0,0 +1,211 @@ +/* -*- 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/. */ + +#ifndef NS_SMILANIMATIONCONTROLLER_H_ +#define NS_SMILANIMATIONCONTROLLER_H_ + +#include "mozilla/Attributes.h" +#include "nsAutoPtr.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsITimer.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsSMILTimeContainer.h" +#include "nsSMILCompositorTable.h" +#include "nsSMILMilestone.h" +#include "nsRefreshDriver.h" + +struct nsSMILTargetIdentifier; +class nsIDocument; + +namespace mozilla { +class RestyleTracker; +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +//---------------------------------------------------------------------- +// nsSMILAnimationController +// +// The animation controller maintains the animation timer and determines the +// sample times and sample rate for all SMIL animations in a document. There is +// at most one animation controller per nsDocument so that frame-rate tuning can +// be performed at a document-level. +// +// The animation controller can contain many child time containers (timed +// document root objects) which may correspond to SVG document fragments within +// a compound document. These time containers can be paused individually or +// here, at the document level. +// +class nsSMILAnimationController final : public nsSMILTimeContainer, + public nsARefreshObserver +{ +public: + explicit nsSMILAnimationController(nsIDocument* aDoc); + + // Clears mDocument pointer. (Called by our nsIDocument when it's going away) + void Disconnect(); + + // nsSMILContainer + virtual void Pause(uint32_t aType) override; + virtual void Resume(uint32_t aType) override; + virtual nsSMILTime GetParentTime() const override; + + // nsARefreshObserver + NS_IMETHOD_(MozExternalRefCountType) AddRef() override; + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + virtual void WillRefresh(mozilla::TimeStamp aTime) override; + + // Methods for registering and enumerating animation elements + void RegisterAnimationElement(mozilla::dom::SVGAnimationElement* aAnimationElement); + void UnregisterAnimationElement(mozilla::dom::SVGAnimationElement* aAnimationElement); + + // Methods for resampling all animations + // (A resample performs the same operations as a sample but doesn't advance + // the current time and doesn't check if the container is paused) + // This will flush pending style changes for the document. + void Resample() { DoSample(false); } + + void SetResampleNeeded() + { + if (!mRunningSample && !mResampleNeeded) { + FlagDocumentNeedsFlush(); + mResampleNeeded = true; + } + } + + // This will flush pending style changes for the document. + void FlushResampleRequests() + { + if (!mResampleNeeded) + return; + + Resample(); + } + + // Methods for handling page transitions + void OnPageShow(); + void OnPageHide(); + + // Methods for supporting cycle-collection + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + // Methods for relaying the availability of the refresh driver + void NotifyRefreshDriverCreated(nsRefreshDriver* aRefreshDriver); + void NotifyRefreshDriverDestroying(nsRefreshDriver* aRefreshDriver); + + // Helper to check if we have any animation elements at all + bool HasRegisteredAnimations() const + { + return mAnimationElementTable.Count() != 0; + } + + void AddStyleUpdatesTo(mozilla::RestyleTracker& aTracker); + bool MightHavePendingStyleUpdates() const + { + return mMightHavePendingStyleUpdates; + } + +protected: + ~nsSMILAnimationController(); + + // Typedefs + typedef nsPtrHashKey TimeContainerPtrKey; + typedef nsTHashtable TimeContainerHashtable; + typedef nsPtrHashKey AnimationElementPtrKey; + typedef nsTHashtable AnimationElementHashtable; + + // Returns mDocument's refresh driver, if it's got one. + nsRefreshDriver* GetRefreshDriver(); + + // Methods for controlling whether we're sampling + void StartSampling(nsRefreshDriver* aRefreshDriver); + void StopSampling(nsRefreshDriver* aRefreshDriver); + + // Wrapper for StartSampling that defers if no animations are registered. + void MaybeStartSampling(nsRefreshDriver* aRefreshDriver); + + // Sample-related callbacks and implementation helpers + virtual void DoSample() override; + void DoSample(bool aSkipUnchangedContainers); + + void RewindElements(); + + void DoMilestoneSamples(); + + static void SampleTimedElement(mozilla::dom::SVGAnimationElement* aElement, + TimeContainerHashtable* aActiveContainers); + + static void AddAnimationToCompositorTable( + mozilla::dom::SVGAnimationElement* aElement, + nsSMILCompositorTable* aCompositorTable, + bool& aStyleFlushNeeded); + + static bool GetTargetIdentifierForAnimation( + mozilla::dom::SVGAnimationElement* aAnimElem, nsSMILTargetIdentifier& aResult); + + // Methods for adding/removing time containers + virtual nsresult AddChild(nsSMILTimeContainer& aChild) override; + virtual void RemoveChild(nsSMILTimeContainer& aChild) override; + + void FlagDocumentNeedsFlush(); + + // Members + nsAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + AnimationElementHashtable mAnimationElementTable; + TimeContainerHashtable mChildContainerTable; + mozilla::TimeStamp mCurrentSampleTime; + mozilla::TimeStamp mStartTime; + + // Average time between samples from the refresh driver. This is used to + // detect large unexpected gaps between samples such as can occur when the + // computer sleeps. The nature of the SMIL model means that catching up these + // large gaps can be expensive as, for example, many events may need to be + // dispatched for the intervening time when no samples were received. + // + // In such cases, we ignore the intervening gap and continue sampling from + // when we were expecting the next sample to arrive. + // + // Note that we only do this for SMIL and not CSS transitions (which doesn't + // have so much work to do to catch up) nor scripted animations (which expect + // animation time to follow real time). + // + // This behaviour does not affect pausing (since we're not *expecting* any + // samples then) nor seeking (where the SMIL model behaves somewhat + // differently such as not dispatching events). + nsSMILTime mAvgTimeBetweenSamples; + + bool mResampleNeeded; + // If we're told to start sampling but there are no animation elements we just + // record the time, set the following flag, and then wait until we have an + // animation element. Then we'll reset this flag and actually start sampling. + bool mDeferredStartSampling; + bool mRunningSample; + + // Are we registered with our document's refresh driver? + bool mRegisteredWithRefreshDriver; + + // Have we updated animated values without adding them to the restyle tracker? + bool mMightHavePendingStyleUpdates; + + // Store raw ptr to mDocument. It owns the controller, so controller + // shouldn't outlive it + nsIDocument* mDocument; + + // Contains compositors used in our last sample. We keep this around + // so we can detect when an element/attribute used to be animated, + // but isn't anymore for some reason. (e.g. if its element is + // removed or retargeted) + nsAutoPtr mLastCompositorTable; +}; + +#endif // NS_SMILANIMATIONCONTROLLER_H_ diff --git a/dom/smil/nsSMILAnimationFunction.cpp b/dom/smil/nsSMILAnimationFunction.cpp new file mode 100644 index 000000000..767181897 --- /dev/null +++ b/dom/smil/nsSMILAnimationFunction.cpp @@ -0,0 +1,1070 @@ +/* -*- 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 "nsSMILAnimationFunction.h" + +#include "mozilla/dom/SVGAnimationElement.h" +#include "mozilla/Move.h" +#include "nsISMILAttr.h" +#include "nsSMILCSSValueType.h" +#include "nsSMILParserUtils.h" +#include "nsSMILNullType.h" +#include "nsSMILTimedElement.h" +#include "nsAttrValueInlines.h" +#include "nsGkAtoms.h" +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "nsIContent.h" +#include "nsContentUtils.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include +#include + +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// Static members + +nsAttrValue::EnumTable nsSMILAnimationFunction::sAccumulateTable[] = { + {"none", false}, + {"sum", true}, + {nullptr, 0} +}; + +nsAttrValue::EnumTable nsSMILAnimationFunction::sAdditiveTable[] = { + {"replace", false}, + {"sum", true}, + {nullptr, 0} +}; + +nsAttrValue::EnumTable nsSMILAnimationFunction::sCalcModeTable[] = { + {"linear", CALC_LINEAR}, + {"discrete", CALC_DISCRETE}, + {"paced", CALC_PACED}, + {"spline", CALC_SPLINE}, + {nullptr, 0} +}; + +// Any negative number should be fine as a sentinel here, +// because valid distances are non-negative. +#define COMPUTE_DISTANCE_ERROR (-1) + +//---------------------------------------------------------------------- +// Constructors etc. + +nsSMILAnimationFunction::nsSMILAnimationFunction() + : mSampleTime(-1), + mRepeatIteration(0), + mBeginTime(INT64_MIN), + mAnimationElement(nullptr), + mErrorFlags(0), + mIsActive(false), + mIsFrozen(false), + mLastValue(false), + mHasChanged(true), + mValueNeedsReparsingEverySample(false), + mPrevSampleWasSingleValueAnimation(false), + mWasSkippedInPrevSample(false) +{ +} + +void +nsSMILAnimationFunction::SetAnimationElement( + SVGAnimationElement* aAnimationElement) +{ + mAnimationElement = aAnimationElement; +} + +bool +nsSMILAnimationFunction::SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, nsresult* aParseResult) +{ + bool foundMatch = true; + nsresult parseResult = NS_OK; + + // The attributes 'by', 'from', 'to', and 'values' may be parsed differently + // depending on the element & attribute we're animating. So instead of + // parsing them now we re-parse them at every sample. + if (aAttribute == nsGkAtoms::by || + aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || + aAttribute == nsGkAtoms::values) { + // We parse to, from, by, values at sample time. + // XXX Need to flag which attribute has changed and then when we parse it at + // sample time, report any errors and reset the flag + mHasChanged = true; + aResult.SetTo(aValue); + } else if (aAttribute == nsGkAtoms::accumulate) { + parseResult = SetAccumulate(aValue, aResult); + } else if (aAttribute == nsGkAtoms::additive) { + parseResult = SetAdditive(aValue, aResult); + } else if (aAttribute == nsGkAtoms::calcMode) { + parseResult = SetCalcMode(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keyTimes) { + parseResult = SetKeyTimes(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keySplines) { + parseResult = SetKeySplines(aValue, aResult); + } else { + foundMatch = false; + } + + if (foundMatch && aParseResult) { + *aParseResult = parseResult; + } + + return foundMatch; +} + +bool +nsSMILAnimationFunction::UnsetAttr(nsIAtom* aAttribute) +{ + bool foundMatch = true; + + if (aAttribute == nsGkAtoms::by || + aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || + aAttribute == nsGkAtoms::values) { + mHasChanged = true; + } else if (aAttribute == nsGkAtoms::accumulate) { + UnsetAccumulate(); + } else if (aAttribute == nsGkAtoms::additive) { + UnsetAdditive(); + } else if (aAttribute == nsGkAtoms::calcMode) { + UnsetCalcMode(); + } else if (aAttribute == nsGkAtoms::keyTimes) { + UnsetKeyTimes(); + } else if (aAttribute == nsGkAtoms::keySplines) { + UnsetKeySplines(); + } else { + foundMatch = false; + } + + return foundMatch; +} + +void +nsSMILAnimationFunction::SampleAt(nsSMILTime aSampleTime, + const nsSMILTimeValue& aSimpleDuration, + uint32_t aRepeatIteration) +{ + // * Update mHasChanged ("Might this sample be different from prev one?") + // Were we previously sampling a fill="freeze" final val? (We're not anymore.) + mHasChanged |= mLastValue; + + // Are we sampling at a new point in simple duration? And does that matter? + mHasChanged |= + (mSampleTime != aSampleTime || mSimpleDuration != aSimpleDuration) && + !IsValueFixedForSimpleDuration(); + + // Are we on a new repeat and accumulating across repeats? + if (!mErrorFlags) { // (can't call GetAccumulate() if we've had parse errors) + mHasChanged |= (mRepeatIteration != aRepeatIteration) && GetAccumulate(); + } + + mSampleTime = aSampleTime; + mSimpleDuration = aSimpleDuration; + mRepeatIteration = aRepeatIteration; + mLastValue = false; +} + +void +nsSMILAnimationFunction::SampleLastValue(uint32_t aRepeatIteration) +{ + if (mHasChanged || !mLastValue || mRepeatIteration != aRepeatIteration) { + mHasChanged = true; + } + + mRepeatIteration = aRepeatIteration; + mLastValue = true; +} + +void +nsSMILAnimationFunction::Activate(nsSMILTime aBeginTime) +{ + mBeginTime = aBeginTime; + mIsActive = true; + mIsFrozen = false; + mHasChanged = true; +} + +void +nsSMILAnimationFunction::Inactivate(bool aIsFrozen) +{ + mIsActive = false; + mIsFrozen = aIsFrozen; + mHasChanged = true; +} + +void +nsSMILAnimationFunction::ComposeResult(const nsISMILAttr& aSMILAttr, + nsSMILValue& aResult) +{ + mHasChanged = false; + mPrevSampleWasSingleValueAnimation = false; + mWasSkippedInPrevSample = false; + + // Skip animations that are inactive or in error + if (!IsActiveOrFrozen() || mErrorFlags != 0) + return; + + // Get the animation values + nsSMILValueArray values; + nsresult rv = GetValues(aSMILAttr, values); + if (NS_FAILED(rv)) + return; + + // Check that we have the right number of keySplines and keyTimes + CheckValueListDependentAttrs(values.Length()); + if (mErrorFlags != 0) + return; + + // If this interval is active, we must have a non-negative mSampleTime + MOZ_ASSERT(mSampleTime >= 0 || !mIsActive, + "Negative sample time for active animation"); + MOZ_ASSERT(mSimpleDuration.IsResolved() || mLastValue, + "Unresolved simple duration for active or frozen animation"); + + // If we want to add but don't have a base value then just fail outright. + // This can happen when we skipped getting the base value because there's an + // animation function in the sandwich that should replace it but that function + // failed unexpectedly. + bool isAdditive = IsAdditive(); + if (isAdditive && aResult.IsNull()) + return; + + nsSMILValue result; + + if (values.Length() == 1 && !IsToAnimation()) { + + // Single-valued animation + result = values[0]; + mPrevSampleWasSingleValueAnimation = true; + + } else if (mLastValue) { + + // Sampling last value + const nsSMILValue& last = values[values.Length() - 1]; + result = last; + + // See comment in AccumulateResult: to-animation does not accumulate + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + // If the target attribute type doesn't support addition Add will + // fail leaving result = last + result.Add(last, mRepeatIteration); + } + + } else { + + // Interpolation + if (NS_FAILED(InterpolateResult(values, result, aResult))) + return; + + if (NS_FAILED(AccumulateResult(values, result))) + return; + } + + // If additive animation isn't required or isn't supported, set the value. + if (!isAdditive || NS_FAILED(aResult.SandwichAdd(result))) { + aResult = Move(result); + } +} + +int8_t +nsSMILAnimationFunction::CompareTo(const nsSMILAnimationFunction* aOther) const +{ + NS_ENSURE_TRUE(aOther, 0); + + NS_ASSERTION(aOther != this, "Trying to compare to self"); + + // Inactive animations sort first + if (!IsActiveOrFrozen() && aOther->IsActiveOrFrozen()) + return -1; + + if (IsActiveOrFrozen() && !aOther->IsActiveOrFrozen()) + return 1; + + // Sort based on begin time + if (mBeginTime != aOther->GetBeginTime()) + return mBeginTime > aOther->GetBeginTime() ? 1 : -1; + + // Next sort based on syncbase dependencies: the dependent element sorts after + // its syncbase + const nsSMILTimedElement& thisTimedElement = + mAnimationElement->TimedElement(); + const nsSMILTimedElement& otherTimedElement = + aOther->mAnimationElement->TimedElement(); + if (thisTimedElement.IsTimeDependent(otherTimedElement)) + return 1; + if (otherTimedElement.IsTimeDependent(thisTimedElement)) + return -1; + + // Animations that appear later in the document sort after those earlier in + // the document + MOZ_ASSERT(mAnimationElement != aOther->mAnimationElement, + "Two animations cannot have the same animation content element!"); + + return (nsContentUtils::PositionIsBefore(mAnimationElement, aOther->mAnimationElement)) + ? -1 : 1; +} + +bool +nsSMILAnimationFunction::WillReplace() const +{ + /* + * In IsAdditive() we don't consider to-animation to be additive as it is + * a special case that is dealt with differently in the compositing method. + * Here, however, we return FALSE for to-animation (i.e. it will NOT replace + * the underlying value) as it builds on the underlying value. + */ + return !mErrorFlags && !(IsAdditive() || IsToAnimation()); +} + +bool +nsSMILAnimationFunction::HasChanged() const +{ + return mHasChanged || mValueNeedsReparsingEverySample; +} + +bool +nsSMILAnimationFunction::UpdateCachedTarget( + const nsSMILTargetIdentifier& aNewTarget) +{ + if (!mLastTarget.Equals(aNewTarget)) { + mLastTarget = aNewTarget; + return true; + } + return false; +} + +//---------------------------------------------------------------------- +// Implementation helpers + +nsresult +nsSMILAnimationFunction::InterpolateResult(const nsSMILValueArray& aValues, + nsSMILValue& aResult, + nsSMILValue& aBaseValue) +{ + // Sanity check animation values + if ((!IsToAnimation() && aValues.Length() < 2) || + (IsToAnimation() && aValues.Length() != 1)) { + NS_ERROR("Unexpected number of values"); + return NS_ERROR_FAILURE; + } + + if (IsToAnimation() && aBaseValue.IsNull()) { + return NS_ERROR_FAILURE; + } + + // Get the normalised progress through the simple duration. + // + // If we have an indefinite simple duration, just set the progress to be + // 0 which will give us the expected behaviour of the animation being fixed at + // its starting point. + double simpleProgress = 0.0; + + if (mSimpleDuration.IsDefinite()) { + nsSMILTime dur = mSimpleDuration.GetMillis(); + + MOZ_ASSERT(dur >= 0, "Simple duration should not be negative"); + MOZ_ASSERT(mSampleTime >= 0, "Sample time should not be negative"); + + if (mSampleTime >= dur || mSampleTime < 0) { + NS_ERROR("Animation sampled outside interval"); + return NS_ERROR_FAILURE; + } + + if (dur > 0) { + simpleProgress = (double)mSampleTime / dur; + } // else leave simpleProgress at 0.0 (e.g. if mSampleTime == dur == 0) + } + + nsresult rv = NS_OK; + nsSMILCalcMode calcMode = GetCalcMode(); + + // Force discrete calcMode for visibility since StyleAnimationValue will + // try to interpolate it using the special clamping behavior defined for + // CSS. + if (nsSMILCSSValueType::PropertyFromValue(aValues[0]) + == eCSSProperty_visibility) { + calcMode = CALC_DISCRETE; + } + + if (calcMode != CALC_DISCRETE) { + // Get the normalised progress between adjacent values + const nsSMILValue* from = nullptr; + const nsSMILValue* to = nullptr; + // Init to -1 to make sure that if we ever forget to set this, the + // MOZ_ASSERT that tests that intervalProgress is in range will fail. + double intervalProgress = -1.f; + if (IsToAnimation()) { + from = &aBaseValue; + to = &aValues[0]; + if (calcMode == CALC_PACED) { + // Note: key[Times/Splines/Points] are ignored for calcMode="paced" + intervalProgress = simpleProgress; + } else { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + intervalProgress = ScaleIntervalProgress(scaledSimpleProgress, 0); + } + } else if (calcMode == CALC_PACED) { + rv = ComputePacedPosition(aValues, simpleProgress, + intervalProgress, from, to); + // Note: If the above call fails, we'll skip the "from->Interpolate" + // call below, and we'll drop into the CALC_DISCRETE section + // instead. (as the spec says we should, because our failure was + // presumably due to the values being non-additive) + } else { // calcMode == CALC_LINEAR or calcMode == CALC_SPLINE + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + uint32_t index = (uint32_t)floor(scaledSimpleProgress * + (aValues.Length() - 1)); + from = &aValues[index]; + to = &aValues[index + 1]; + intervalProgress = + scaledSimpleProgress * (aValues.Length() - 1) - index; + intervalProgress = ScaleIntervalProgress(intervalProgress, index); + } + + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(from, "NULL from-value during interpolation"); + MOZ_ASSERT(to, "NULL to-value during interpolation"); + MOZ_ASSERT(0.0f <= intervalProgress && intervalProgress < 1.0f, + "Interval progress should be in the range [0, 1)"); + rv = from->Interpolate(*to, intervalProgress, aResult); + } + } + + // Discrete-CalcMode case + // Note: If interpolation failed (isn't supported for this type), the SVG + // spec says to force discrete mode. + if (calcMode == CALC_DISCRETE || NS_FAILED(rv)) { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, CALC_DISCRETE); + + // Floating-point errors can mean that, for example, a sample time of 29s in + // a 100s duration animation gives us a simple progress of 0.28999999999 + // instead of the 0.29 we'd expect. Normally this isn't a noticeable + // problem, but when we have sudden jumps in animation values (such as is + // the case here with discrete animation) we can get unexpected results. + // + // To counteract this, before we perform a floor() on the animation + // progress, we add a tiny fudge factor to push us into the correct interval + // in cases where floating-point errors might cause us to fall short. + static const double kFloatingPointFudgeFactor = 1.0e-16; + if (scaledSimpleProgress + kFloatingPointFudgeFactor <= 1.0) { + scaledSimpleProgress += kFloatingPointFudgeFactor; + } + + if (IsToAnimation()) { + // We don't follow SMIL 3, 12.6.4, where discrete to animations + // are the same as animations. Instead, we treat it as a + // discrete animation with two values (the underlying value and + // the to="" value), and honor keyTimes="" as well. + uint32_t index = (uint32_t)floor(scaledSimpleProgress * 2); + aResult = index == 0 ? aBaseValue : aValues[0]; + } else { + uint32_t index = (uint32_t)floor(scaledSimpleProgress * aValues.Length()); + aResult = aValues[index]; + } + rv = NS_OK; + } + return rv; +} + +nsresult +nsSMILAnimationFunction::AccumulateResult(const nsSMILValueArray& aValues, + nsSMILValue& aResult) +{ + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + const nsSMILValue& lastValue = aValues[aValues.Length() - 1]; + + // If the target attribute type doesn't support addition, Add will + // fail and we leave aResult untouched. + aResult.Add(lastValue, mRepeatIteration); + } + + return NS_OK; +} + +/* + * Given the simple progress for a paced animation, this method: + * - determines which two elements of the values array we're in between + * (returned as aFrom and aTo) + * - determines where we are between them + * (returned as aIntervalProgress) + * + * Returns NS_OK, or NS_ERROR_FAILURE if our values don't support distance + * computation. + */ +nsresult +nsSMILAnimationFunction::ComputePacedPosition(const nsSMILValueArray& aValues, + double aSimpleProgress, + double& aIntervalProgress, + const nsSMILValue*& aFrom, + const nsSMILValue*& aTo) +{ + NS_ASSERTION(0.0f <= aSimpleProgress && aSimpleProgress < 1.0f, + "aSimpleProgress is out of bounds"); + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + MOZ_ASSERT(aValues.Length() >= 2, "Unexpected number of values"); + + // Trivial case: If we have just 2 values, then there's only one interval + // for us to traverse, and our progress across that interval is the exact + // same as our overall progress. + if (aValues.Length() == 2) { + aIntervalProgress = aSimpleProgress; + aFrom = &aValues[0]; + aTo = &aValues[1]; + return NS_OK; + } + + double totalDistance = ComputePacedTotalDistance(aValues); + if (totalDistance == COMPUTE_DISTANCE_ERROR) + return NS_ERROR_FAILURE; + + // If we have 0 total distance, then it's unclear where our "paced" position + // should be. We can just fail, which drops us into discrete animation mode. + // (That's fine, since our values are apparently indistinguishable anyway.) + if (totalDistance == 0.0) { + return NS_ERROR_FAILURE; + } + + // total distance we should have moved at this point in time. + // (called 'remainingDist' due to how it's used in loop below) + double remainingDist = aSimpleProgress * totalDistance; + + // Must be satisfied, because totalDistance is a sum of (non-negative) + // distances, and aSimpleProgress is non-negative + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + // Find where remainingDist puts us in the list of values + // Note: We could optimize this next loop by caching the + // interval-distances in an array, but maybe that's excessive. + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + // Note: The following assertion is valid because remainingDist should + // start out non-negative, and this loop never shaves off more than its + // current value. + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + double curIntervalDist; + +#ifdef DEBUG + nsresult rv = +#endif + aValues[i].ComputeDistance(aValues[i+1], curIntervalDist); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "If we got through ComputePacedTotalDistance, we should " + "be able to recompute each sub-distance without errors"); + + NS_ASSERTION(curIntervalDist >= 0, "distance values must be non-negative"); + // Clamp distance value at 0, just in case ComputeDistance is evil. + curIntervalDist = std::max(curIntervalDist, 0.0); + + if (remainingDist >= curIntervalDist) { + remainingDist -= curIntervalDist; + } else { + // NOTE: If we get here, then curIntervalDist necessarily is not 0. Why? + // Because this clause is only hit when remainingDist < curIntervalDist, + // and if curIntervalDist were 0, that would mean remainingDist would + // have to be < 0. But that can't happen, because remainingDist (as + // a distance) is non-negative by definition. + NS_ASSERTION(curIntervalDist != 0, + "We should never get here with this set to 0..."); + + // We found the right spot -- an interpolated position between + // values i and i+1. + aFrom = &aValues[i]; + aTo = &aValues[i+1]; + aIntervalProgress = remainingDist / curIntervalDist; + return NS_OK; + } + } + + NS_NOTREACHED("shouldn't complete loop & get here -- if we do, " + "then aSimpleProgress was probably out of bounds"); + return NS_ERROR_FAILURE; +} + +/* + * Computes the total distance to be travelled by a paced animation. + * + * Returns the total distance, or returns COMPUTE_DISTANCE_ERROR if + * our values don't support distance computation. + */ +double +nsSMILAnimationFunction::ComputePacedTotalDistance( + const nsSMILValueArray& aValues) const +{ + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + + double totalDistance = 0.0; + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + double tmpDist; + nsresult rv = aValues[i].ComputeDistance(aValues[i+1], tmpDist); + if (NS_FAILED(rv)) { + return COMPUTE_DISTANCE_ERROR; + } + + // Clamp distance value to 0, just in case we have an evil ComputeDistance + // implementation somewhere + MOZ_ASSERT(tmpDist >= 0.0f, "distance values must be non-negative"); + tmpDist = std::max(tmpDist, 0.0); + + totalDistance += tmpDist; + } + + return totalDistance; +} + +double +nsSMILAnimationFunction::ScaleSimpleProgress(double aProgress, + nsSMILCalcMode aCalcMode) +{ + if (!HasAttr(nsGkAtoms::keyTimes)) + return aProgress; + + uint32_t numTimes = mKeyTimes.Length(); + + if (numTimes < 2) + return aProgress; + + uint32_t i = 0; + for (; i < numTimes - 2 && aProgress >= mKeyTimes[i+1]; ++i) { } + + if (aCalcMode == CALC_DISCRETE) { + // discrete calcMode behaviour differs in that each keyTime defines the time + // from when the corresponding value is set, and therefore the last value + // needn't be 1. So check if we're in the last 'interval', that is, the + // space between the final value and 1.0. + if (aProgress >= mKeyTimes[i+1]) { + MOZ_ASSERT(i == numTimes - 2, + "aProgress is not in range of the current interval, yet the " + "current interval is not the last bounded interval either."); + ++i; + } + return (double)i / numTimes; + } + + double& intervalStart = mKeyTimes[i]; + double& intervalEnd = mKeyTimes[i+1]; + + double intervalLength = intervalEnd - intervalStart; + if (intervalLength <= 0.0) + return intervalStart; + + return (i + (aProgress - intervalStart) / intervalLength) / + double(numTimes - 1); +} + +double +nsSMILAnimationFunction::ScaleIntervalProgress(double aProgress, + uint32_t aIntervalIndex) +{ + if (GetCalcMode() != CALC_SPLINE) + return aProgress; + + if (!HasAttr(nsGkAtoms::keySplines)) + return aProgress; + + MOZ_ASSERT(aIntervalIndex < mKeySplines.Length(), + "Invalid interval index"); + + nsSMILKeySpline const &spline = mKeySplines[aIntervalIndex]; + return spline.GetSplineValue(aProgress); +} + +bool +nsSMILAnimationFunction::HasAttr(nsIAtom* aAttName) const +{ + return mAnimationElement->HasAnimAttr(aAttName); +} + +const nsAttrValue* +nsSMILAnimationFunction::GetAttr(nsIAtom* aAttName) const +{ + return mAnimationElement->GetAnimAttr(aAttName); +} + +bool +nsSMILAnimationFunction::GetAttr(nsIAtom* aAttName, nsAString& aResult) const +{ + return mAnimationElement->GetAnimAttr(aAttName, aResult); +} + +/* + * A utility function to make querying an attribute that corresponds to an + * nsSMILValue a little neater. + * + * @param aAttName The attribute name (in the global namespace). + * @param aSMILAttr The SMIL attribute to perform the parsing. + * @param[out] aResult The resulting nsSMILValue. + * @param[out] aPreventCachingOfSandwich + * If |aResult| contains dependencies on its context that + * should prevent the result of the animation sandwich from + * being cached and reused in future samples (as reported + * by nsISMILAttr::ValueFromString), then this outparam + * will be set to true. Otherwise it is left unmodified. + * + * Returns false if a parse error occurred, otherwise returns true. + */ +bool +nsSMILAnimationFunction::ParseAttr(nsIAtom* aAttName, + const nsISMILAttr& aSMILAttr, + nsSMILValue& aResult, + bool& aPreventCachingOfSandwich) const +{ + nsAutoString attValue; + if (GetAttr(aAttName, attValue)) { + bool preventCachingOfSandwich = false; + nsresult rv = aSMILAttr.ValueFromString(attValue, mAnimationElement, + aResult, preventCachingOfSandwich); + if (NS_FAILED(rv)) + return false; + + if (preventCachingOfSandwich) { + aPreventCachingOfSandwich = true; + } + } + return true; +} + +/* + * SMILANIM specifies the following rules for animation function values: + * + * (1) if values is set, it overrides everything + * (2) for from/to/by animation at least to or by must be specified, from on its + * own (or nothing) is an error--which we will ignore + * (3) if both by and to are specified only to will be used, by will be ignored + * (4) if by is specified without from (by animation), forces additive behaviour + * (5) if to is specified without from (to animation), special care needs to be + * taken when compositing animation as such animations are composited last. + * + * This helper method applies these rules to fill in the values list and to set + * some internal state. + */ +nsresult +nsSMILAnimationFunction::GetValues(const nsISMILAttr& aSMILAttr, + nsSMILValueArray& aResult) +{ + if (!mAnimationElement) + return NS_ERROR_FAILURE; + + mValueNeedsReparsingEverySample = false; + nsSMILValueArray result; + + // If "values" is set, use it + if (HasAttr(nsGkAtoms::values)) { + nsAutoString attValue; + GetAttr(nsGkAtoms::values, attValue); + bool preventCachingOfSandwich = false; + if (!nsSMILParserUtils::ParseValues(attValue, mAnimationElement, + aSMILAttr, result, + preventCachingOfSandwich)) { + return NS_ERROR_FAILURE; + } + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + // Else try to/from/by + } else { + bool preventCachingOfSandwich = false; + bool parseOk = true; + nsSMILValue to, from, by; + parseOk &= ParseAttr(nsGkAtoms::to, aSMILAttr, to, + preventCachingOfSandwich); + parseOk &= ParseAttr(nsGkAtoms::from, aSMILAttr, from, + preventCachingOfSandwich); + parseOk &= ParseAttr(nsGkAtoms::by, aSMILAttr, by, + preventCachingOfSandwich); + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + + if (!parseOk || !result.SetCapacity(2, mozilla::fallible)) { + return NS_ERROR_FAILURE; + } + + // AppendElement() below must succeed, because SetCapacity() succeeded. + if (!to.IsNull()) { + if (!from.IsNull()) { + MOZ_ALWAYS_TRUE(result.AppendElement(from, mozilla::fallible)); + MOZ_ALWAYS_TRUE(result.AppendElement(to, mozilla::fallible)); + } else { + MOZ_ALWAYS_TRUE(result.AppendElement(to, mozilla::fallible)); + } + } else if (!by.IsNull()) { + nsSMILValue effectiveFrom(by.mType); + if (!from.IsNull()) + effectiveFrom = from; + // Set values to 'from; from + by' + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveFrom, mozilla::fallible)); + nsSMILValue effectiveTo(effectiveFrom); + if (!effectiveTo.IsNull() && NS_SUCCEEDED(effectiveTo.Add(by))) { + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveTo, mozilla::fallible)); + } else { + // Using by-animation with non-additive type or bad base-value + return NS_ERROR_FAILURE; + } + } else { + // No values, no to, no by -- call it a day + return NS_ERROR_FAILURE; + } + } + + result.SwapElements(aResult); + + return NS_OK; +} + +void +nsSMILAnimationFunction::CheckValueListDependentAttrs(uint32_t aNumValues) +{ + CheckKeyTimes(aNumValues); + CheckKeySplines(aNumValues); +} + +/** + * Performs checks for the keyTimes attribute required by the SMIL spec but + * which depend on other attributes and therefore needs to be updated as + * dependent attributes are set. + */ +void +nsSMILAnimationFunction::CheckKeyTimes(uint32_t aNumValues) +{ + if (!HasAttr(nsGkAtoms::keyTimes)) + return; + + nsSMILCalcMode calcMode = GetCalcMode(); + + // attribute is ignored for calcMode = paced + if (calcMode == CALC_PACED) { + SetKeyTimesErrorFlag(false); + return; + } + + uint32_t numKeyTimes = mKeyTimes.Length(); + if (numKeyTimes < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeyTimesErrorFlag(true); + return; + } + + // no. keyTimes == no. values + // For to-animation the number of values is considered to be 2. + bool matchingNumOfValues = + numKeyTimes == (IsToAnimation() ? 2 : aNumValues); + if (!matchingNumOfValues) { + SetKeyTimesErrorFlag(true); + return; + } + + // first value must be 0 + if (mKeyTimes[0] != 0.0) { + SetKeyTimesErrorFlag(true); + return; + } + + // last value must be 1 for linear or spline calcModes + if (calcMode != CALC_DISCRETE && numKeyTimes > 1 && + mKeyTimes[numKeyTimes - 1] != 1.0) { + SetKeyTimesErrorFlag(true); + return; + } + + SetKeyTimesErrorFlag(false); +} + +void +nsSMILAnimationFunction::CheckKeySplines(uint32_t aNumValues) +{ + // attribute is ignored if calc mode is not spline + if (GetCalcMode() != CALC_SPLINE) { + SetKeySplinesErrorFlag(false); + return; + } + + // calc mode is spline but the attribute is not set + if (!HasAttr(nsGkAtoms::keySplines)) { + SetKeySplinesErrorFlag(false); + return; + } + + if (mKeySplines.Length() < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeySplinesErrorFlag(true); + return; + } + + // ignore splines if there's only one value + if (aNumValues == 1 && !IsToAnimation()) { + SetKeySplinesErrorFlag(false); + return; + } + + // no. keySpline specs == no. values - 1 + uint32_t splineSpecs = mKeySplines.Length(); + if ((splineSpecs != aNumValues - 1 && !IsToAnimation()) || + (IsToAnimation() && splineSpecs != 1)) { + SetKeySplinesErrorFlag(true); + return; + } + + SetKeySplinesErrorFlag(false); +} + +bool +nsSMILAnimationFunction::IsValueFixedForSimpleDuration() const +{ + return mSimpleDuration.IsIndefinite() || + (!mHasChanged && mPrevSampleWasSingleValueAnimation); +} + +//---------------------------------------------------------------------- +// Property getters + +bool +nsSMILAnimationFunction::GetAccumulate() const +{ + const nsAttrValue* value = GetAttr(nsGkAtoms::accumulate); + if (!value) + return false; + + return value->GetEnumValue(); +} + +bool +nsSMILAnimationFunction::GetAdditive() const +{ + const nsAttrValue* value = GetAttr(nsGkAtoms::additive); + if (!value) + return false; + + return value->GetEnumValue(); +} + +nsSMILAnimationFunction::nsSMILCalcMode +nsSMILAnimationFunction::GetCalcMode() const +{ + const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode); + if (!value) + return CALC_LINEAR; + + return nsSMILCalcMode(value->GetEnumValue()); +} + +//---------------------------------------------------------------------- +// Property setters / un-setters: + +nsresult +nsSMILAnimationFunction::SetAccumulate(const nsAString& aAccumulate, + nsAttrValue& aResult) +{ + mHasChanged = true; + bool parseResult = + aResult.ParseEnumValue(aAccumulate, sAccumulateTable, true); + SetAccumulateErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILAnimationFunction::UnsetAccumulate() +{ + SetAccumulateErrorFlag(false); + mHasChanged = true; +} + +nsresult +nsSMILAnimationFunction::SetAdditive(const nsAString& aAdditive, + nsAttrValue& aResult) +{ + mHasChanged = true; + bool parseResult + = aResult.ParseEnumValue(aAdditive, sAdditiveTable, true); + SetAdditiveErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILAnimationFunction::UnsetAdditive() +{ + SetAdditiveErrorFlag(false); + mHasChanged = true; +} + +nsresult +nsSMILAnimationFunction::SetCalcMode(const nsAString& aCalcMode, + nsAttrValue& aResult) +{ + mHasChanged = true; + bool parseResult + = aResult.ParseEnumValue(aCalcMode, sCalcModeTable, true); + SetCalcModeErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILAnimationFunction::UnsetCalcMode() +{ + SetCalcModeErrorFlag(false); + mHasChanged = true; +} + +nsresult +nsSMILAnimationFunction::SetKeySplines(const nsAString& aKeySplines, + nsAttrValue& aResult) +{ + mKeySplines.Clear(); + aResult.SetTo(aKeySplines); + + mHasChanged = true; + + if (!nsSMILParserUtils::ParseKeySplines(aKeySplines, mKeySplines)) { + mKeySplines.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void +nsSMILAnimationFunction::UnsetKeySplines() +{ + mKeySplines.Clear(); + SetKeySplinesErrorFlag(false); + mHasChanged = true; +} + +nsresult +nsSMILAnimationFunction::SetKeyTimes(const nsAString& aKeyTimes, + nsAttrValue& aResult) +{ + mKeyTimes.Clear(); + aResult.SetTo(aKeyTimes); + + mHasChanged = true; + + if (!nsSMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyTimes, true, + mKeyTimes)) { + mKeyTimes.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void +nsSMILAnimationFunction::UnsetKeyTimes() +{ + mKeyTimes.Clear(); + SetKeyTimesErrorFlag(false); + mHasChanged = true; +} diff --git a/dom/smil/nsSMILAnimationFunction.h b/dom/smil/nsSMILAnimationFunction.h new file mode 100644 index 000000000..2380b64ef --- /dev/null +++ b/dom/smil/nsSMILAnimationFunction.h @@ -0,0 +1,458 @@ +/* -*- 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/. */ + +#ifndef NS_SMILANIMATIONFUNCTION_H_ +#define NS_SMILANIMATIONFUNCTION_H_ + +#include "nsISMILAttr.h" +#include "nsGkAtoms.h" +#include "nsString.h" +#include "nsSMILTargetIdentifier.h" +#include "nsSMILTimeValue.h" +#include "nsSMILKeySpline.h" +#include "nsSMILValue.h" +#include "nsTArray.h" +#include "nsAttrValue.h" +#include "nsSMILTypes.h" + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +//---------------------------------------------------------------------- +// nsSMILAnimationFunction +// +// The animation function calculates animation values. It it is provided with +// time parameters (sample time, repeat iteration etc.) and it uses this to +// build an appropriate animation value by performing interpolation and +// addition operations. +// +// It is responsible for implementing the animation parameters of an animation +// element (e.g. from, by, to, values, calcMode, additive, accumulate, keyTimes, +// keySplines) +// +class nsSMILAnimationFunction +{ +public: + nsSMILAnimationFunction(); + + /* + * Sets the owning animation element which this class uses to query attribute + * values and compare document positions. + */ + void SetAnimationElement(mozilla::dom::SVGAnimationElement* aAnimationElement); + + /* + * Sets animation-specific attributes (or marks them dirty, in the case + * of from/to/by/values). + * + * @param aAttribute The attribute being set + * @param aValue The updated value of the attribute. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aParseResult Outparam used for reporting parse errors. Will be set + * to NS_OK if everything succeeds. + * @return true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, nsresult* aParseResult = nullptr); + + /* + * Unsets the given attribute. + * + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool UnsetAttr(nsIAtom* aAttribute); + + /** + * Indicate a new sample has occurred. + * + * @param aSampleTime The sample time for this timed element expressed in + * simple time. + * @param aSimpleDuration The simple duration for this timed element. + * @param aRepeatIteration The repeat iteration for this sample. The first + * iteration has a value of 0. + */ + void SampleAt(nsSMILTime aSampleTime, + const nsSMILTimeValue& aSimpleDuration, + uint32_t aRepeatIteration); + + /** + * Indicate to sample using the last value defined for the animation function. + * This value is not normally sampled due to the end-point exclusive timing + * model but only occurs when the fill mode is "freeze" and the active + * duration is an even multiple of the simple duration. + * + * @param aRepeatIteration The repeat iteration for this sample. The first + * iteration has a value of 0. + */ + void SampleLastValue(uint32_t aRepeatIteration); + + /** + * Indicate that this animation is now active. This is used to instruct the + * animation function that it should now add its result to the animation + * sandwich. The begin time is also provided for proper prioritization of + * animation functions, and for this reason, this method must be called + * before either of the Sample methods. + * + * @param aBeginTime The begin time for the newly active interval. + */ + void Activate(nsSMILTime aBeginTime); + + /** + * Indicate that this animation is no longer active. This is used to instruct + * the animation function that it should no longer add its result to the + * animation sandwich. + * + * @param aIsFrozen true if this animation should continue to contribute + * to the animation sandwich using the most recent sample + * parameters. + */ + void Inactivate(bool aIsFrozen); + + /** + * Combines the result of this animation function for the last sample with the + * specified value. + * + * @param aSMILAttr This animation's target attribute. Used here for + * doing attribute-specific parsing of from/to/by/values. + * + * @param aResult The value to compose with. + */ + void ComposeResult(const nsISMILAttr& aSMILAttr, nsSMILValue& aResult); + + /** + * Returns the relative priority of this animation to another. The priority is + * used for determining the position of the animation in the animation + * sandwich -- higher priority animations are applied on top of lower + * priority animations. + * + * @return -1 if this animation has lower priority or 1 if this animation has + * higher priority + * + * This method should never return any other value, including 0. + */ + int8_t CompareTo(const nsSMILAnimationFunction* aOther) const; + + /* + * The following methods are provided so that the compositor can optimize its + * operations by only composing those animation that will affect the final + * result. + */ + + /** + * Indicates if the animation is currently active or frozen. Inactive + * animations will not contribute to the composed result. + * + * @return true if the animation is active or frozen, false otherwise. + */ + bool IsActiveOrFrozen() const + { + /* + * - Frozen animations should be considered active for the purposes of + * compositing. + * - This function does not assume that our nsSMILValues (by/from/to/values) + * have already been parsed. + */ + return (mIsActive || mIsFrozen); + } + + /** + * Indicates if the animation is active. + * + * @return true if the animation is active, false otherwise. + */ + bool IsActive() const { + return mIsActive; + } + + /** + * Indicates if this animation will replace the passed in result rather than + * adding to it. Animations that replace the underlying value may be called + * without first calling lower priority animations. + * + * @return True if the animation will replace, false if it will add or + * otherwise build on the passed in value. + */ + virtual bool WillReplace() const; + + /** + * Indicates if the parameters for this animation have changed since the last + * time it was composited. This allows rendering to be performed only when + * necessary, particularly when no animations are active. + * + * Note that the caller is responsible for determining if the animation + * target has changed (with help from my UpdateCachedTarget() method). + * + * @return true if the animation parameters have changed, false + * otherwise. + */ + bool HasChanged() const; + + /** + * This method lets us clear the 'HasChanged' flag for inactive animations + * after we've reacted to their change to the 'inactive' state, so that we + * won't needlessly recompose their targets in every sample. + * + * This should only be called on an animation function that is inactive and + * that returns true from HasChanged(). + */ + void ClearHasChanged() + { + MOZ_ASSERT(HasChanged(), + "clearing mHasChanged flag, when it's already false"); + MOZ_ASSERT(!IsActiveOrFrozen(), + "clearing mHasChanged flag for active animation"); + mHasChanged = false; + } + + /** + * Updates the cached record of our animation target, and returns a boolean + * that indicates whether the target has changed since the last call to this + * function. (This lets nsSMILCompositor check whether its animation + * functions have changed value or target since the last sample. If none of + * them have, then the compositor doesn't need to do anything.) + * + * @param aNewTarget A nsSMILTargetIdentifier representing the animation + * target of this function for this sample. + * @return true if |aNewTarget| is different from the old cached value; + * otherwise, false. + */ + bool UpdateCachedTarget(const nsSMILTargetIdentifier& aNewTarget); + + /** + * Returns true if this function was skipped in the previous sample (because + * there was a higher-priority non-additive animation). If a skipped animation + * function is later used, then the animation sandwich must be recomposited. + */ + bool WasSkippedInPrevSample() const { + return mWasSkippedInPrevSample; + } + + /** + * Mark this animation function as having been skipped. By marking the + * function as skipped, if it is used in a subsequent sample we'll know to + * recomposite the sandwich. + */ + void SetWasSkipped() { + mWasSkippedInPrevSample = true; + } + + /** + * Returns true if we need to recalculate the animation value on every sample. + * (e.g. because it depends on context like the font-size) + */ + bool ValueNeedsReparsingEverySample() const { + return mValueNeedsReparsingEverySample; + } + + // Comparator utility class, used for sorting nsSMILAnimationFunctions + class Comparator { + public: + bool Equals(const nsSMILAnimationFunction* aElem1, + const nsSMILAnimationFunction* aElem2) const { + return (aElem1->CompareTo(aElem2) == 0); + } + bool LessThan(const nsSMILAnimationFunction* aElem1, + const nsSMILAnimationFunction* aElem2) const { + return (aElem1->CompareTo(aElem2) < 0); + } + }; + +protected: + // Typedefs + typedef FallibleTArray nsSMILValueArray; + + // Types + enum nsSMILCalcMode : uint8_t + { + CALC_LINEAR, + CALC_DISCRETE, + CALC_PACED, + CALC_SPLINE + }; + + // Used for sorting nsSMILAnimationFunctions + nsSMILTime GetBeginTime() const { return mBeginTime; } + + // Property getters + bool GetAccumulate() const; + bool GetAdditive() const; + virtual nsSMILCalcMode GetCalcMode() const; + + // Property setters + nsresult SetAccumulate(const nsAString& aAccumulate, nsAttrValue& aResult); + nsresult SetAdditive(const nsAString& aAdditive, nsAttrValue& aResult); + nsresult SetCalcMode(const nsAString& aCalcMode, nsAttrValue& aResult); + nsresult SetKeyTimes(const nsAString& aKeyTimes, nsAttrValue& aResult); + nsresult SetKeySplines(const nsAString& aKeySplines, nsAttrValue& aResult); + + // Property un-setters + void UnsetAccumulate(); + void UnsetAdditive(); + void UnsetCalcMode(); + void UnsetKeyTimes(); + void UnsetKeySplines(); + + // Helpers + virtual nsresult InterpolateResult(const nsSMILValueArray& aValues, + nsSMILValue& aResult, + nsSMILValue& aBaseValue); + nsresult AccumulateResult(const nsSMILValueArray& aValues, + nsSMILValue& aResult); + + nsresult ComputePacedPosition(const nsSMILValueArray& aValues, + double aSimpleProgress, + double& aIntervalProgress, + const nsSMILValue*& aFrom, + const nsSMILValue*& aTo); + double ComputePacedTotalDistance(const nsSMILValueArray& aValues) const; + + /** + * Adjust the simple progress, that is, the point within the simple duration, + * by applying any keyTimes. + */ + double ScaleSimpleProgress(double aProgress, nsSMILCalcMode aCalcMode); + /** + * Adjust the progress within an interval, that is, between two animation + * values, by applying any keySplines. + */ + double ScaleIntervalProgress(double aProgress, uint32_t aIntervalIndex); + + // Convenience attribute getters -- use these instead of querying + // mAnimationElement as these may need to be overridden by subclasses + virtual bool HasAttr(nsIAtom* aAttName) const; + virtual const nsAttrValue* GetAttr(nsIAtom* aAttName) const; + virtual bool GetAttr(nsIAtom* aAttName, + nsAString& aResult) const; + + bool ParseAttr(nsIAtom* aAttName, const nsISMILAttr& aSMILAttr, + nsSMILValue& aResult, + bool& aPreventCachingOfSandwich) const; + + virtual nsresult GetValues(const nsISMILAttr& aSMILAttr, + nsSMILValueArray& aResult); + + virtual void CheckValueListDependentAttrs(uint32_t aNumValues); + void CheckKeyTimes(uint32_t aNumValues); + void CheckKeySplines(uint32_t aNumValues); + + virtual bool IsToAnimation() const { + return !HasAttr(nsGkAtoms::values) && + HasAttr(nsGkAtoms::to) && + !HasAttr(nsGkAtoms::from); + } + + // Returns true if we know our composited value won't change over the + // simple duration of this animation (for a fixed base value). + virtual bool IsValueFixedForSimpleDuration() const; + + inline bool IsAdditive() const { + /* + * Animation is additive if: + * + * (1) additive = "sum" (GetAdditive() == true), or + * (2) it is 'by animation' (by is set, from and values are not) + * + * Although animation is not additive if it is 'to animation' + */ + bool isByAnimation = (!HasAttr(nsGkAtoms::values) && + HasAttr(nsGkAtoms::by) && + !HasAttr(nsGkAtoms::from)); + return !IsToAnimation() && (GetAdditive() || isByAnimation); + } + + // Setters for error flags + // These correspond to bit-indices in mErrorFlags, for tracking parse errors + // in these attributes, when those parse errors should block us from doing + // animation. + enum AnimationAttributeIdx { + BF_ACCUMULATE = 0, + BF_ADDITIVE = 1, + BF_CALC_MODE = 2, + BF_KEY_TIMES = 3, + BF_KEY_SPLINES = 4, + BF_KEY_POINTS = 5 // only + }; + + inline void SetAccumulateErrorFlag(bool aNewValue) { + SetErrorFlag(BF_ACCUMULATE, aNewValue); + } + inline void SetAdditiveErrorFlag(bool aNewValue) { + SetErrorFlag(BF_ADDITIVE, aNewValue); + } + inline void SetCalcModeErrorFlag(bool aNewValue) { + SetErrorFlag(BF_CALC_MODE, aNewValue); + } + inline void SetKeyTimesErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_TIMES, aNewValue); + } + inline void SetKeySplinesErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_SPLINES, aNewValue); + } + inline void SetKeyPointsErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_POINTS, aNewValue); + } + inline void SetErrorFlag(AnimationAttributeIdx aField, bool aValue) { + if (aValue) { + mErrorFlags |= (0x01 << aField); + } else { + mErrorFlags &= ~(0x01 << aField); + } + } + + // Members + // ------- + + static nsAttrValue::EnumTable sAdditiveTable[]; + static nsAttrValue::EnumTable sCalcModeTable[]; + static nsAttrValue::EnumTable sAccumulateTable[]; + + FallibleTArray mKeyTimes; + FallibleTArray mKeySplines; + + // These are the parameters provided by the previous sample. Currently we + // perform lazy calculation. That is, we only calculate the result if and when + // instructed by the compositor. This allows us to apply the result directly + // to the animation value and allows the compositor to filter out functions + // that it determines will not contribute to the final result. + nsSMILTime mSampleTime; // sample time within simple dur + nsSMILTimeValue mSimpleDuration; + uint32_t mRepeatIteration; + + nsSMILTime mBeginTime; // document time + + // The owning animation element. This is used for sorting based on document + // position and for fetching attribute values stored in the element. + // Raw pointer is OK here, because this nsSMILAnimationFunction can't outlive + // its owning animation element. + mozilla::dom::SVGAnimationElement* mAnimationElement; + + // Which attributes have been set but have had errors. This is not used for + // all attributes but only those which have specified error behaviour + // associated with them. + uint16_t mErrorFlags; + + // Allows us to check whether an animation function has changed target from + // sample to sample (because if neither target nor animated value have + // changed, we don't have to do anything). + nsSMILWeakTargetIdentifier mLastTarget; + + // Boolean flags + bool mIsActive:1; + bool mIsFrozen:1; + bool mLastValue:1; + bool mHasChanged:1; + bool mValueNeedsReparsingEverySample:1; + bool mPrevSampleWasSingleValueAnimation:1; + bool mWasSkippedInPrevSample:1; +}; + +#endif // NS_SMILANIMATIONFUNCTION_H_ diff --git a/dom/smil/nsSMILCSSProperty.cpp b/dom/smil/nsSMILCSSProperty.cpp new file mode 100644 index 000000000..53f3e0fbf --- /dev/null +++ b/dom/smil/nsSMILCSSProperty.cpp @@ -0,0 +1,275 @@ +/* -*- 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/. */ + +/* representation of a SMIL-animatable CSS property on an element */ + +#include "nsSMILCSSProperty.h" + +#include "mozilla/dom/Element.h" +#include "mozilla/Move.h" +#include "nsSMILCSSValueType.h" +#include "nsSMILValue.h" +#include "nsComputedDOMStyle.h" +#include "nsCSSProps.h" +#include "nsIDOMElement.h" +#include "nsIDocument.h" + +using namespace mozilla::dom; + +// Helper function +static bool +GetCSSComputedValue(Element* aElem, + nsCSSPropertyID aPropID, + nsAString& aResult) +{ + MOZ_ASSERT(!nsCSSProps::IsShorthand(aPropID), + "Can't look up computed value of shorthand property"); + MOZ_ASSERT(nsSMILCSSProperty::IsPropertyAnimatable(aPropID), + "Shouldn't get here for non-animatable properties"); + + nsIDocument* doc = aElem->GetUncomposedDoc(); + if (!doc) { + // This can happen if we process certain types of restyles mid-sample + // and remove anonymous animated content from the document as a result. + // See bug 534975. + return false; + } + + nsIPresShell* shell = doc->GetShell(); + if (!shell) { + NS_WARNING("Unable to look up computed style -- no pres shell"); + return false; + } + + RefPtr computedStyle = + NS_NewComputedDOMStyle(aElem, EmptyString(), shell); + + computedStyle->GetPropertyValue(aPropID, aResult); + return true; +} + +// Class Methods +nsSMILCSSProperty::nsSMILCSSProperty(nsCSSPropertyID aPropID, + Element* aElement) + : mPropID(aPropID), mElement(aElement) +{ + MOZ_ASSERT(IsPropertyAnimatable(mPropID), + "Creating a nsSMILCSSProperty for a property " + "that's not supported for animation"); +} + +nsSMILValue +nsSMILCSSProperty::GetBaseValue() const +{ + // To benefit from Return Value Optimization and avoid copy constructor calls + // due to our use of return-by-value, we must return the exact same object + // from ALL return points. This function must only return THIS variable: + nsSMILValue baseValue; + + // SPECIAL CASE: (a) Shorthands + // (b) 'display' + if (nsCSSProps::IsShorthand(mPropID) || mPropID == eCSSProperty_display) { + // We can't look up the base (computed-style) value of shorthand + // properties because they aren't guaranteed to have a consistent computed + // value. + // + // Also, although we can look up the base value of the display property, + // doing so involves clearing and resetting the property which can cause + // frames to be recreated which we'd like to avoid. + // + // In either case, just return a dummy value (initialized with the right + // type, so as not to indicate failure). + nsSMILValue tmpVal(&nsSMILCSSValueType::sSingleton); + Swap(baseValue, tmpVal); + return baseValue; + } + + // GENERAL CASE: Non-Shorthands + // (1) Put empty string in override style for property mPropID + // (saving old override style value, so we can set it again when we're done) + nsICSSDeclaration* overrideDecl = mElement->GetSMILOverrideStyle(); + nsAutoString cachedOverrideStyleVal; + if (overrideDecl) { + overrideDecl->GetPropertyValue(mPropID, cachedOverrideStyleVal); + // (Don't bother clearing override style if it's already empty) + if (!cachedOverrideStyleVal.IsEmpty()) { + overrideDecl->SetPropertyValue(mPropID, EmptyString()); + } + } + + // (2) Get Computed Style + nsAutoString computedStyleVal; + bool didGetComputedVal = GetCSSComputedValue(mElement, mPropID, + computedStyleVal); + + // (3) Put cached override style back (if it's non-empty) + if (overrideDecl && !cachedOverrideStyleVal.IsEmpty()) { + overrideDecl->SetPropertyValue(mPropID, cachedOverrideStyleVal); + } + + // (4) Populate our nsSMILValue from the computed style + if (didGetComputedVal) { + // When we parse animation values we check if they are context-sensitive or + // not so that we don't cache animation values whose meaning may change. + // For base values however this is unnecessary since on each sample the + // compositor will fetch the (computed) base value and compare it against + // the cached (computed) value and detect changes for us. + nsSMILCSSValueType::ValueFromString(mPropID, mElement, + computedStyleVal, baseValue, + nullptr); + } + return baseValue; +} + +nsresult +nsSMILCSSProperty::ValueFromString(const nsAString& aStr, + const SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const +{ + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + nsSMILCSSValueType::ValueFromString(mPropID, mElement, aStr, aValue, + &aPreventCachingOfSandwich); + + if (aValue.IsNull()) { + return NS_ERROR_FAILURE; + } + + // XXX Due to bug 536660 (or at least that seems to be the most likely + // culprit), when we have animation setting display:none on a element, + // if we DON'T set the property every sample, chaos ensues. + if (!aPreventCachingOfSandwich && mPropID == eCSSProperty_display) { + aPreventCachingOfSandwich = true; + } + return NS_OK; +} + +nsresult +nsSMILCSSProperty::SetAnimValue(const nsSMILValue& aValue) +{ + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + // Convert nsSMILValue to string + nsAutoString valStr; + if (!nsSMILCSSValueType::ValueToString(aValue, valStr)) { + NS_WARNING("Failed to convert nsSMILValue for CSS property into a string"); + return NS_ERROR_FAILURE; + } + + // Use string value to style the target element + nsICSSDeclaration* overrideDecl = mElement->GetSMILOverrideStyle(); + if (overrideDecl) { + nsAutoString oldValStr; + overrideDecl->GetPropertyValue(mPropID, oldValStr); + if (valStr.Equals(oldValStr)) { + return NS_OK; + } + overrideDecl->SetPropertyValue(mPropID, valStr); + } + return NS_OK; +} + +void +nsSMILCSSProperty::ClearAnimValue() +{ + // Put empty string in override style for our property + nsICSSDeclaration* overrideDecl = mElement->GetSMILOverrideStyle(); + if (overrideDecl) { + overrideDecl->SetPropertyValue(mPropID, EmptyString()); + } +} + +// Based on http://www.w3.org/TR/SVG/propidx.html +// static +bool +nsSMILCSSProperty::IsPropertyAnimatable(nsCSSPropertyID aPropID) +{ + // NOTE: Right now, Gecko doesn't recognize the following properties from + // the SVG Property Index: + // alignment-baseline + // baseline-shift + // color-profile + // color-rendering + // glyph-orientation-horizontal + // glyph-orientation-vertical + // kerning + // writing-mode + + switch (aPropID) { + case eCSSProperty_clip: + case eCSSProperty_clip_rule: + case eCSSProperty_clip_path: + case eCSSProperty_color: + case eCSSProperty_color_interpolation: + case eCSSProperty_color_interpolation_filters: + case eCSSProperty_cursor: + case eCSSProperty_display: + case eCSSProperty_dominant_baseline: + case eCSSProperty_fill: + case eCSSProperty_fill_opacity: + case eCSSProperty_fill_rule: + case eCSSProperty_filter: + case eCSSProperty_flood_color: + case eCSSProperty_flood_opacity: + case eCSSProperty_font: + case eCSSProperty_font_family: + case eCSSProperty_font_size: + case eCSSProperty_font_size_adjust: + case eCSSProperty_font_stretch: + case eCSSProperty_font_style: + case eCSSProperty_font_variant: + case eCSSProperty_font_weight: + case eCSSProperty_height: + case eCSSProperty_image_rendering: + case eCSSProperty_letter_spacing: + case eCSSProperty_lighting_color: + case eCSSProperty_marker: + case eCSSProperty_marker_end: + case eCSSProperty_marker_mid: + case eCSSProperty_marker_start: + case eCSSProperty_mask: + case eCSSProperty_mask_type: + case eCSSProperty_opacity: + case eCSSProperty_overflow: + case eCSSProperty_pointer_events: + case eCSSProperty_shape_rendering: + case eCSSProperty_stop_color: + case eCSSProperty_stop_opacity: + case eCSSProperty_stroke: + case eCSSProperty_stroke_dasharray: + case eCSSProperty_stroke_dashoffset: + case eCSSProperty_stroke_linecap: + case eCSSProperty_stroke_linejoin: + case eCSSProperty_stroke_miterlimit: + case eCSSProperty_stroke_opacity: + case eCSSProperty_stroke_width: + case eCSSProperty_text_anchor: + case eCSSProperty_text_decoration: + case eCSSProperty_text_decoration_line: + case eCSSProperty_text_rendering: + case eCSSProperty_vector_effect: + case eCSSProperty_width: + case eCSSProperty_visibility: + case eCSSProperty_word_spacing: + return true; + + // EXPLICITLY NON-ANIMATABLE PROPERTIES: + // (Some of these aren't supported at all in Gecko -- I've commented those + // ones out. If/when we add support for them, uncomment their line here) + // ---------------------------------------------------------------------- + // case eCSSProperty_enable_background: + // case eCSSProperty_glyph_orientation_horizontal: + // case eCSSProperty_glyph_orientation_vertical: + // case eCSSProperty_writing_mode: + case eCSSProperty_direction: + case eCSSProperty_unicode_bidi: + return false; + + default: + return false; + } +} diff --git a/dom/smil/nsSMILCSSProperty.h b/dom/smil/nsSMILCSSProperty.h new file mode 100644 index 000000000..028a9aaa2 --- /dev/null +++ b/dom/smil/nsSMILCSSProperty.h @@ -0,0 +1,67 @@ +/* -*- 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/. */ + +/* representation of a SMIL-animatable CSS property on an element */ + +#ifndef NS_SMILCSSPROPERTY_H_ +#define NS_SMILCSSPROPERTY_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILAttr.h" +#include "nsIAtom.h" +#include "nsCSSPropertyID.h" +#include "nsCSSValue.h" + +namespace mozilla { +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + +/** + * nsSMILCSSProperty: Implements the nsISMILAttr interface for SMIL animations + * that target CSS properties. Represents a particular animation-targeted CSS + * property on a particular element. + */ +class nsSMILCSSProperty : public nsISMILAttr +{ +public: + /** + * Constructs a new nsSMILCSSProperty. + * @param aPropID The CSS property we're interested in animating. + * @param aElement The element whose CSS property is being animated. + */ + nsSMILCSSProperty(nsCSSPropertyID aPropID, mozilla::dom::Element* aElement); + + // nsISMILAttr methods + virtual nsresult ValueFromString(const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const override; + virtual nsSMILValue GetBaseValue() const override; + virtual nsresult SetAnimValue(const nsSMILValue& aValue) override; + virtual void ClearAnimValue() override; + + /** + * Utility method - returns true if the given property is supported for + * SMIL animation. + * + * @param aProperty The property to check for animation support. + * @return true if the given property is supported for SMIL animation, or + * false otherwise + */ + static bool IsPropertyAnimatable(nsCSSPropertyID aPropID); + +protected: + nsCSSPropertyID mPropID; + // Using non-refcounted pointer for mElement -- we know mElement will stay + // alive for my lifetime because a nsISMILAttr (like me) only lives as long + // as the Compositing step, and DOM elements don't get a chance to die during + // that time. + mozilla::dom::Element* mElement; +}; + +#endif // NS_SMILCSSPROPERTY_H_ diff --git a/dom/smil/nsSMILCSSValueType.cpp b/dom/smil/nsSMILCSSValueType.cpp new file mode 100644 index 000000000..ed89e7710 --- /dev/null +++ b/dom/smil/nsSMILCSSValueType.cpp @@ -0,0 +1,447 @@ +/* -*- 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/. */ + +/* representation of a value for a SMIL-animated CSS property */ + +#include "nsSMILCSSValueType.h" +#include "nsString.h" +#include "nsSMILParserUtils.h" +#include "nsSMILValue.h" +#include "nsCSSValue.h" +#include "nsColor.h" +#include "nsPresContext.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/dom/Element.h" +#include "nsDebug.h" +#include "nsStyleUtil.h" +#include "nsIDocument.h" + +using namespace mozilla::dom; +using mozilla::StyleAnimationValue; + +/*static*/ nsSMILCSSValueType nsSMILCSSValueType::sSingleton; + +struct ValueWrapper { + ValueWrapper(nsCSSPropertyID aPropID, const StyleAnimationValue& aValue) : + mPropID(aPropID), mCSSValue(aValue) {} + + nsCSSPropertyID mPropID; + StyleAnimationValue mCSSValue; +}; + +// Helper Methods +// -------------- +static const StyleAnimationValue* +GetZeroValueForUnit(StyleAnimationValue::Unit aUnit) +{ + static const StyleAnimationValue + sZeroCoord(0, StyleAnimationValue::CoordConstructor); + static const StyleAnimationValue + sZeroPercent(0.0f, StyleAnimationValue::PercentConstructor); + static const StyleAnimationValue + sZeroFloat(0.0f, StyleAnimationValue::FloatConstructor); + static const StyleAnimationValue + sZeroColor(NS_RGB(0,0,0), StyleAnimationValue::ColorConstructor); + + MOZ_ASSERT(aUnit != StyleAnimationValue::eUnit_Null, + "Need non-null unit for a zero value"); + switch (aUnit) { + case StyleAnimationValue::eUnit_Coord: + return &sZeroCoord; + case StyleAnimationValue::eUnit_Percent: + return &sZeroPercent; + case StyleAnimationValue::eUnit_Float: + return &sZeroFloat; + case StyleAnimationValue::eUnit_Color: + return &sZeroColor; + default: + return nullptr; + } +} + +// This method requires at least one of its arguments to be non-null. +// +// If one argument is null, this method updates it to point to "zero" +// for the other argument's Unit (if applicable; otherwise, we return false). +// +// If neither argument is null, this method generally does nothing, though it +// may apply a workaround for the special case where a 0 length-value is mixed +// with a eUnit_Float value. (See comment below.) +// +// Returns true on success, or false. +static bool +FinalizeStyleAnimationValues(const StyleAnimationValue*& aValue1, + const StyleAnimationValue*& aValue2) +{ + MOZ_ASSERT(aValue1 || aValue2, + "expecting at least one non-null value"); + + // Are we missing either val? (If so, it's an implied 0 in other val's units) + if (!aValue1) { + aValue1 = GetZeroValueForUnit(aValue2->GetUnit()); + return !!aValue1; // Fail if we have no zero value for this unit. + } + if (!aValue2) { + aValue2 = GetZeroValueForUnit(aValue1->GetUnit()); + return !!aValue2; // Fail if we have no zero value for this unit. + } + + // Ok, both values were specified. + // Need to handle a special-case, though: unitless nonzero length (parsed as + // eUnit_Float) mixed with unitless 0 length (parsed as eUnit_Coord). These + // won't interoperate in StyleAnimationValue, since their Units don't match. + // In this case, we replace the eUnit_Coord 0 value with eUnit_Float 0 value. + const StyleAnimationValue& zeroCoord = + *GetZeroValueForUnit(StyleAnimationValue::eUnit_Coord); + if (*aValue1 == zeroCoord && + aValue2->GetUnit() == StyleAnimationValue::eUnit_Float) { + aValue1 = GetZeroValueForUnit(StyleAnimationValue::eUnit_Float); + } else if (*aValue2 == zeroCoord && + aValue1->GetUnit() == StyleAnimationValue::eUnit_Float) { + aValue2 = GetZeroValueForUnit(StyleAnimationValue::eUnit_Float); + } + + return true; +} + +static void +InvertSign(StyleAnimationValue& aValue) +{ + switch (aValue.GetUnit()) { + case StyleAnimationValue::eUnit_Coord: + aValue.SetCoordValue(-aValue.GetCoordValue()); + break; + case StyleAnimationValue::eUnit_Percent: + aValue.SetPercentValue(-aValue.GetPercentValue()); + break; + case StyleAnimationValue::eUnit_Float: + aValue.SetFloatValue(-aValue.GetFloatValue()); + break; + default: + NS_NOTREACHED("Calling InvertSign with an unsupported unit"); + break; + } +} + +static ValueWrapper* +ExtractValueWrapper(nsSMILValue& aValue) +{ + return static_cast(aValue.mU.mPtr); +} + +static const ValueWrapper* +ExtractValueWrapper(const nsSMILValue& aValue) +{ + return static_cast(aValue.mU.mPtr); +} + +// Class methods +// ------------- +void +nsSMILCSSValueType::Init(nsSMILValue& aValue) const +{ + MOZ_ASSERT(aValue.IsNull(), "Unexpected SMIL value type"); + + aValue.mU.mPtr = nullptr; + aValue.mType = this; +} + +void +nsSMILCSSValueType::Destroy(nsSMILValue& aValue) const +{ + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value type"); + delete static_cast(aValue.mU.mPtr); + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +nsSMILCSSValueType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value type"); + const ValueWrapper* srcWrapper = ExtractValueWrapper(aSrc); + ValueWrapper* destWrapper = ExtractValueWrapper(aDest); + + if (srcWrapper) { + if (!destWrapper) { + // barely-initialized dest -- need to alloc & copy + aDest.mU.mPtr = new ValueWrapper(*srcWrapper); + } else { + // both already fully-initialized -- just copy straight across + *destWrapper = *srcWrapper; + } + } else if (destWrapper) { + // fully-initialized dest, barely-initialized src -- clear dest + delete destWrapper; + aDest.mU.mPtr = destWrapper = nullptr; + } // else, both are barely-initialized -- nothing to do. + + return NS_OK; +} + +bool +nsSMILCSSValueType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected SMIL value"); + const ValueWrapper* leftWrapper = ExtractValueWrapper(aLeft); + const ValueWrapper* rightWrapper = ExtractValueWrapper(aRight); + + if (leftWrapper) { + if (rightWrapper) { + // Both non-null + NS_WARNING_ASSERTION(leftWrapper != rightWrapper, + "Two nsSMILValues with matching ValueWrapper ptr"); + return (leftWrapper->mPropID == rightWrapper->mPropID && + leftWrapper->mCSSValue == rightWrapper->mCSSValue); + } + // Left non-null, right null + return false; + } + if (rightWrapper) { + // Left null, right non-null + return false; + } + // Both null + return true; +} + +nsresult +nsSMILCSSValueType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + + ValueWrapper* destWrapper = ExtractValueWrapper(aDest); + const ValueWrapper* valueToAddWrapper = ExtractValueWrapper(aValueToAdd); + MOZ_ASSERT(destWrapper || valueToAddWrapper, + "need at least one fully-initialized value"); + + nsCSSPropertyID property = (valueToAddWrapper ? valueToAddWrapper->mPropID : + destWrapper->mPropID); + // Special case: font-size-adjust and stroke-dasharray are explicitly + // non-additive (even though StyleAnimationValue *could* support adding them) + if (property == eCSSProperty_font_size_adjust || + property == eCSSProperty_stroke_dasharray) { + return NS_ERROR_FAILURE; + } + + const StyleAnimationValue* valueToAdd = valueToAddWrapper ? + &valueToAddWrapper->mCSSValue : nullptr; + const StyleAnimationValue* destValue = destWrapper ? + &destWrapper->mCSSValue : nullptr; + if (!FinalizeStyleAnimationValues(valueToAdd, destValue)) { + return NS_ERROR_FAILURE; + } + // Did FinalizeStyleAnimationValues change destValue? + // If so, update outparam to use the new value. + if (destWrapper && &destWrapper->mCSSValue != destValue) { + destWrapper->mCSSValue = *destValue; + } + + // Handle barely-initialized "zero" destination. + if (!destWrapper) { + aDest.mU.mPtr = destWrapper = + new ValueWrapper(property, *destValue); + } + + return StyleAnimationValue::Add(property, + destWrapper->mCSSValue, *valueToAdd, aCount) ? + NS_OK : NS_ERROR_FAILURE; +} + +nsresult +nsSMILCSSValueType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + MOZ_ASSERT(aFrom.mType == aTo.mType, + "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + + const ValueWrapper* fromWrapper = ExtractValueWrapper(aFrom); + const ValueWrapper* toWrapper = ExtractValueWrapper(aTo); + MOZ_ASSERT(toWrapper, "expecting non-null endpoint"); + + const StyleAnimationValue* fromCSSValue = fromWrapper ? + &fromWrapper->mCSSValue : nullptr; + const StyleAnimationValue* toCSSValue = &toWrapper->mCSSValue; + if (!FinalizeStyleAnimationValues(fromCSSValue, toCSSValue)) { + return NS_ERROR_FAILURE; + } + + return StyleAnimationValue::ComputeDistance(toWrapper->mPropID, + *fromCSSValue, *toCSSValue, + nullptr, + aDistance) ? + NS_OK : NS_ERROR_FAILURE; +} + +nsresult +nsSMILCSSValueType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, + "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + MOZ_ASSERT(aUnitDistance >= 0.0 && aUnitDistance <= 1.0, + "unit distance value out of bounds"); + MOZ_ASSERT(!aResult.mU.mPtr, "expecting barely-initialized outparam"); + + const ValueWrapper* startWrapper = ExtractValueWrapper(aStartVal); + const ValueWrapper* endWrapper = ExtractValueWrapper(aEndVal); + MOZ_ASSERT(endWrapper, "expecting non-null endpoint"); + + const StyleAnimationValue* startCSSValue = startWrapper ? + &startWrapper->mCSSValue : nullptr; + const StyleAnimationValue* endCSSValue = &endWrapper->mCSSValue; + if (!FinalizeStyleAnimationValues(startCSSValue, endCSSValue)) { + return NS_ERROR_FAILURE; + } + + StyleAnimationValue resultValue; + if (StyleAnimationValue::Interpolate(endWrapper->mPropID, + *startCSSValue, *endCSSValue, + aUnitDistance, resultValue)) { + aResult.mU.mPtr = new ValueWrapper(endWrapper->mPropID, resultValue); + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +// Helper function to extract presContext +static nsPresContext* +GetPresContextForElement(Element* aElem) +{ + nsIDocument* doc = aElem->GetUncomposedDoc(); + if (!doc) { + // This can happen if we process certain types of restyles mid-sample + // and remove anonymous animated content from the document as a result. + // See bug 534975. + return nullptr; + } + nsIPresShell* shell = doc->GetShell(); + return shell ? shell->GetPresContext() : nullptr; +} + +// Helper function to parse a string into a StyleAnimationValue +static bool +ValueFromStringHelper(nsCSSPropertyID aPropID, + Element* aTargetElement, + nsPresContext* aPresContext, + const nsAString& aString, + StyleAnimationValue& aStyleAnimValue, + bool* aIsContextSensitive) +{ + // If value is negative, we'll strip off the "-" so the CSS parser won't + // barf, and then manually make the parsed value negative. + // (This is a partial solution to let us accept some otherwise out-of-bounds + // CSS values. Bug 501188 will provide a more complete fix.) + bool isNegative = false; + uint32_t subStringBegin = 0; + + // NOTE: We need to opt-out 'stroke-dasharray' from the negative-number + // check. Its values might look negative (e.g. by starting with "-1"), but + // they're more complicated than our simple negation logic here can handle. + if (aPropID != eCSSProperty_stroke_dasharray) { + int32_t absValuePos = nsSMILParserUtils::CheckForNegativeNumber(aString); + if (absValuePos > 0) { + isNegative = true; + subStringBegin = (uint32_t)absValuePos; // Start parsing after '-' sign + } + } + RefPtr styleContext = + nsComputedDOMStyle::GetStyleContextForElement(aTargetElement, nullptr, + aPresContext->PresShell()); + if (!styleContext) { + return false; + } + nsDependentSubstring subString(aString, subStringBegin); + if (!StyleAnimationValue::ComputeValue(aPropID, aTargetElement, styleContext, + subString, true, aStyleAnimValue, + aIsContextSensitive)) { + return false; + } + if (isNegative) { + InvertSign(aStyleAnimValue); + } + + if (aPropID == eCSSProperty_font_size) { + // Divide out text-zoom, since SVG is supposed to ignore it + MOZ_ASSERT(aStyleAnimValue.GetUnit() == StyleAnimationValue::eUnit_Coord, + "'font-size' value with unexpected style unit"); + aStyleAnimValue.SetCoordValue(aStyleAnimValue.GetCoordValue() / + aPresContext->TextZoom()); + } + return true; +} + +// static +void +nsSMILCSSValueType::ValueFromString(nsCSSPropertyID aPropID, + Element* aTargetElement, + const nsAString& aString, + nsSMILValue& aValue, + bool* aIsContextSensitive) +{ + MOZ_ASSERT(aValue.IsNull(), "Outparam should be null-typed"); + nsPresContext* presContext = GetPresContextForElement(aTargetElement); + if (!presContext) { + NS_WARNING("Not parsing animation value; unable to get PresContext"); + return; + } + + nsIDocument* doc = aTargetElement->GetUncomposedDoc(); + if (doc && !nsStyleUtil::CSPAllowsInlineStyle(nullptr, + doc->NodePrincipal(), + doc->GetDocumentURI(), + 0, aString, nullptr)) { + return; + } + + StyleAnimationValue parsedValue; + if (ValueFromStringHelper(aPropID, aTargetElement, presContext, + aString, parsedValue, aIsContextSensitive)) { + sSingleton.Init(aValue); + aValue.mU.mPtr = new ValueWrapper(aPropID, parsedValue); + } +} + +// static +bool +nsSMILCSSValueType::ValueToString(const nsSMILValue& aValue, + nsAString& aString) +{ + MOZ_ASSERT(aValue.mType == &nsSMILCSSValueType::sSingleton, + "Unexpected SMIL value type"); + const ValueWrapper* wrapper = ExtractValueWrapper(aValue); + return !wrapper || + StyleAnimationValue::UncomputeValue(wrapper->mPropID, + wrapper->mCSSValue, aString); +} + +// static +nsCSSPropertyID +nsSMILCSSValueType::PropertyFromValue(const nsSMILValue& aValue) +{ + if (aValue.mType != &nsSMILCSSValueType::sSingleton) { + return eCSSProperty_UNKNOWN; + } + + const ValueWrapper* wrapper = ExtractValueWrapper(aValue); + if (!wrapper) { + return eCSSProperty_UNKNOWN; + } + + return wrapper->mPropID; +} diff --git a/dom/smil/nsSMILCSSValueType.h b/dom/smil/nsSMILCSSValueType.h new file mode 100644 index 000000000..0c71605f0 --- /dev/null +++ b/dom/smil/nsSMILCSSValueType.h @@ -0,0 +1,116 @@ +/* -*- 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/. */ + +/* representation of a value for a SMIL-animated CSS property */ + +#ifndef NS_SMILCSSVALUETYPE_H_ +#define NS_SMILCSSVALUETYPE_H_ + +#include "nsISMILType.h" +#include "nsCSSPropertyID.h" +#include "mozilla/Attributes.h" + +class nsAString; + +namespace mozilla { +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + +/* + * nsSMILCSSValueType: Represents a SMIL-animated CSS value. + */ +class nsSMILCSSValueType : public nsISMILType +{ +public: + typedef mozilla::dom::Element Element; + + // Singleton for nsSMILValue objects to hold onto. + static nsSMILCSSValueType sSingleton; + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue&) const override; + virtual nsresult Assign(nsSMILValue& aDest, + const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, + const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +public: + // Helper Methods + // -------------- + /** + * Sets up the given nsSMILValue to represent the given string value. The + * string is interpreted as a value for the given property on the given + * element. + * + * On failure, this method leaves aValue.mType == nsSMILNullType::sSingleton. + * Otherwise, this method leaves aValue.mType == this class's singleton. + * + * @param aPropID The property for which we're parsing a value. + * @param aTargetElement The target element to whom the property/value + * setting applies. + * @param aString The string to be parsed as a CSS value. + * @param [out] aValue The nsSMILValue to be populated. Should + * initially be null-typed. + * @param [out] aIsContextSensitive Set to true if |aString| may produce + * a different |aValue| depending on other + * CSS properties on |aTargetElement| + * or its ancestors (e.g. 'inherit). + * false otherwise. May be nullptr. + * Not set if the method fails. + * @pre aValue.IsNull() + * @post aValue.IsNull() || aValue.mType == nsSMILCSSValueType::sSingleton + */ + static void ValueFromString(nsCSSPropertyID aPropID, + Element* aTargetElement, + const nsAString& aString, + nsSMILValue& aValue, + bool* aIsContextSensitive); + + /** + * Creates a string representation of the given nsSMILValue. + * + * Note: aValue is expected to be of this type (that is, it's expected to + * have been initialized by nsSMILCSSValueType::sSingleton). If aValue is a + * freshly-initialized value, this method will succeed, though the resulting + * string will be empty. + * + * @param aValue The nsSMILValue to be converted into a string. + * @param [out] aString The string to be populated with the given value. + * @return true on success, false on failure. + */ + static bool ValueToString(const nsSMILValue& aValue, nsAString& aString); + + /** + * Return the CSS property animated by the specified value. + * + * @param aValue The nsSMILValue to examine. + * @return The nsCSSPropertyID enum value of the property animated + * by |aValue|, or eCSSProperty_UNKNOWN if the type of + * |aValue| is not nsSMILCSSValueType. + */ + static nsCSSPropertyID PropertyFromValue(const nsSMILValue& aValue); + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr nsSMILCSSValueType() {} +}; + +#endif // NS_SMILCSSVALUETYPE_H_ diff --git a/dom/smil/nsSMILCompositor.cpp b/dom/smil/nsSMILCompositor.cpp new file mode 100644 index 000000000..fe7b2c828 --- /dev/null +++ b/dom/smil/nsSMILCompositor.cpp @@ -0,0 +1,204 @@ +/* -*- 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 "nsSMILCompositor.h" + +#include "nsCSSProps.h" +#include "nsHashKeys.h" +#include "nsSMILCSSProperty.h" + +// PLDHashEntryHdr methods +bool +nsSMILCompositor::KeyEquals(KeyTypePointer aKey) const +{ + return aKey && aKey->Equals(mKey); +} + +/*static*/ PLDHashNumber +nsSMILCompositor::HashKey(KeyTypePointer aKey) +{ + // Combine the 3 values into one numeric value, which will be hashed. + // NOTE: We right-shift one of the pointers by 2 to get some randomness in + // its 2 lowest-order bits. (Those shifted-off bits will always be 0 since + // our pointers will be word-aligned.) + return (NS_PTR_TO_UINT32(aKey->mElement.get()) >> 2) + + NS_PTR_TO_UINT32(aKey->mAttributeName.get()) + + (aKey->mIsCSS ? 1 : 0); +} + +// Cycle-collection support +void +nsSMILCompositor::Traverse(nsCycleCollectionTraversalCallback* aCallback) +{ + if (!mKey.mElement) + return; + + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCallback, "Compositor mKey.mElement"); + aCallback->NoteXPCOMChild(mKey.mElement); +} + +// Other methods +void +nsSMILCompositor::AddAnimationFunction(nsSMILAnimationFunction* aFunc) +{ + if (aFunc) { + mAnimationFunctions.AppendElement(aFunc); + } +} + +void +nsSMILCompositor::ComposeAttribute(bool& aMightHavePendingStyleUpdates) +{ + if (!mKey.mElement) + return; + + // FIRST: Get the nsISMILAttr (to grab base value from, and to eventually + // give animated value to) + nsAutoPtr smilAttr(CreateSMILAttr()); + if (!smilAttr) { + // Target attribute not found (or, out of memory) + return; + } + if (mAnimationFunctions.IsEmpty()) { + // No active animation functions. (We can still have a nsSMILCompositor in + // that case if an animation function has *just* become inactive) + smilAttr->ClearAnimValue(); + // Removing the animation effect may require a style update. + aMightHavePendingStyleUpdates = true; + return; + } + + // SECOND: Sort the animationFunctions, to prepare for compositing. + nsSMILAnimationFunction::Comparator comparator; + mAnimationFunctions.Sort(comparator); + + // THIRD: Step backwards through animation functions to find out + // which ones we actually care about. + uint32_t firstFuncToCompose = GetFirstFuncToAffectSandwich(); + + // FOURTH: Get & cache base value + nsSMILValue sandwichResultValue; + if (!mAnimationFunctions[firstFuncToCompose]->WillReplace()) { + sandwichResultValue = smilAttr->GetBaseValue(); + } + UpdateCachedBaseValue(sandwichResultValue); + + if (!mForceCompositing) { + return; + } + + // FIFTH: Compose animation functions + aMightHavePendingStyleUpdates = true; + uint32_t length = mAnimationFunctions.Length(); + for (uint32_t i = firstFuncToCompose; i < length; ++i) { + mAnimationFunctions[i]->ComposeResult(*smilAttr, sandwichResultValue); + } + if (sandwichResultValue.IsNull()) { + smilAttr->ClearAnimValue(); + return; + } + + // SIXTH: Set the animated value to the final composited result. + nsresult rv = smilAttr->SetAnimValue(sandwichResultValue); + if (NS_FAILED(rv)) { + NS_WARNING("nsISMILAttr::SetAnimValue failed"); + } +} + +void +nsSMILCompositor::ClearAnimationEffects() +{ + if (!mKey.mElement || !mKey.mAttributeName) + return; + + nsAutoPtr smilAttr(CreateSMILAttr()); + if (!smilAttr) { + // Target attribute not found (or, out of memory) + return; + } + smilAttr->ClearAnimValue(); +} + +// Protected Helper Functions +// -------------------------- +nsISMILAttr* +nsSMILCompositor::CreateSMILAttr() +{ + if (mKey.mIsCSS) { + nsCSSPropertyID propId = + nsCSSProps::LookupProperty(nsDependentAtomString(mKey.mAttributeName), + CSSEnabledState::eForAllContent); + if (nsSMILCSSProperty::IsPropertyAnimatable(propId)) { + return new nsSMILCSSProperty(propId, mKey.mElement.get()); + } + } else { + return mKey.mElement->GetAnimatedAttr(mKey.mAttributeNamespaceID, + mKey.mAttributeName); + } + return nullptr; +} + +uint32_t +nsSMILCompositor::GetFirstFuncToAffectSandwich() +{ + // For performance reasons, we throttle most animations on elements in + // display:none subtrees. (We can't throttle animations that target the + // "display" property itself, though -- if we did, display:none elements + // could never be dynamically displayed via animations.) + // To determine whether we're in a display:none subtree, we will check the + // element's primary frame since element in display:none subtree doesn't have + // a primary frame. Before this process, we will construct frame when we + // append an element to subtree. So we will not need to worry about pending + // frame construction in this step. + bool canThrottle = mKey.mAttributeName != nsGkAtoms::display && + !mKey.mElement->GetPrimaryFrame(); + + uint32_t i; + for (i = mAnimationFunctions.Length(); i > 0; --i) { + nsSMILAnimationFunction* curAnimFunc = mAnimationFunctions[i-1]; + // In the following, the lack of short-circuit behavior of |= means that we + // will ALWAYS run UpdateCachedTarget (even if mForceCompositing is true) + // but only call HasChanged and WasSkippedInPrevSample if necessary. This + // is important since we need UpdateCachedTarget to run in order to detect + // changes to the target in subsequent samples. + mForceCompositing |= + curAnimFunc->UpdateCachedTarget(mKey) || + (curAnimFunc->HasChanged() && !canThrottle) || + curAnimFunc->WasSkippedInPrevSample(); + + if (curAnimFunc->WillReplace()) { + --i; + break; + } + } + + // Mark remaining animation functions as having been skipped so if we later + // use them we'll know to force compositing. + // Note that we only really need to do this if something has changed + // (otherwise we would have set the flag on a previous sample) and if + // something has changed mForceCompositing will be true. + if (mForceCompositing) { + for (uint32_t j = i; j > 0; --j) { + mAnimationFunctions[j-1]->SetWasSkipped(); + } + } + return i; +} + +void +nsSMILCompositor::UpdateCachedBaseValue(const nsSMILValue& aBaseValue) +{ + if (!mCachedBaseValue) { + // We don't have last sample's base value cached. Assume it's changed. + mCachedBaseValue = new nsSMILValue(aBaseValue); + NS_WARNING_ASSERTION(mCachedBaseValue, "failed to cache base value (OOM?)"); + mForceCompositing = true; + } else if (*mCachedBaseValue != aBaseValue) { + // Base value has changed since last sample. + *mCachedBaseValue = aBaseValue; + mForceCompositing = true; + } +} diff --git a/dom/smil/nsSMILCompositor.h b/dom/smil/nsSMILCompositor.h new file mode 100644 index 000000000..ed87ffa8c --- /dev/null +++ b/dom/smil/nsSMILCompositor.h @@ -0,0 +1,107 @@ +/* -*- 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/. */ + +#ifndef NS_SMILCOMPOSITOR_H_ +#define NS_SMILCOMPOSITOR_H_ + +#include "mozilla/Move.h" +#include "nsAutoPtr.h" +#include "nsTHashtable.h" +#include "nsString.h" +#include "nsSMILAnimationFunction.h" +#include "nsSMILTargetIdentifier.h" +#include "nsSMILCompositorTable.h" +#include "PLDHashTable.h" + +//---------------------------------------------------------------------- +// nsSMILCompositor +// +// Performs the composition of the animation sandwich by combining the results +// of a series animation functions according to the rules of SMIL composition +// including prioritising animations. + +class nsSMILCompositor : public PLDHashEntryHdr +{ +public: + typedef nsSMILTargetIdentifier KeyType; + typedef const KeyType& KeyTypeRef; + typedef const KeyType* KeyTypePointer; + + explicit nsSMILCompositor(KeyTypePointer aKey) + : mKey(*aKey), + mForceCompositing(false) + { } + nsSMILCompositor(const nsSMILCompositor& toCopy) + : mKey(toCopy.mKey), + mAnimationFunctions(toCopy.mAnimationFunctions), + mForceCompositing(false) + { } + ~nsSMILCompositor() { } + + // PLDHashEntryHdr methods + KeyTypeRef GetKey() const { return mKey; } + bool KeyEquals(KeyTypePointer aKey) const; + static KeyTypePointer KeyToPointer(KeyTypeRef aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey); + enum { ALLOW_MEMMOVE = false }; + + // Adds the given animation function to this Compositor's list of functions + void AddAnimationFunction(nsSMILAnimationFunction* aFunc); + + // Composes the attribute's current value with the list of animation + // functions, and assigns the resulting value to this compositor's target + // attribute. If a change is made that might produce style updates, + // aMightHavePendingStyleUpdates is set to true. Otherwise it is not modified. + void ComposeAttribute(bool& aMightHavePendingStyleUpdates); + + // Clears animation effects on my target attribute + void ClearAnimationEffects(); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + + // Toggles a bit that will force us to composite (bypassing early-return + // optimizations) when we hit ComposeAttribute. + void ToggleForceCompositing() { mForceCompositing = true; } + + // Transfers |aOther|'s mCachedBaseValue to |this| + void StealCachedBaseValue(nsSMILCompositor* aOther) { + mCachedBaseValue = mozilla::Move(aOther->mCachedBaseValue); + } + + private: + // Create a nsISMILAttr for my target, on the heap. Caller is responsible + // for deallocating the returned object. + nsISMILAttr* CreateSMILAttr(); + + // Finds the index of the first function that will affect our animation + // sandwich. Also toggles the 'mForceCompositing' flag if it finds that any + // (used) functions have changed. + uint32_t GetFirstFuncToAffectSandwich(); + + // If the passed-in base value differs from our cached base value, this + // method updates the cached value (and toggles the 'mForceCompositing' flag) + void UpdateCachedBaseValue(const nsSMILValue& aBaseValue); + + // The hash key (tuple of element/attributeName/attributeType) + KeyType mKey; + + // Hash Value: List of animation functions that animate the specified attr + nsTArray mAnimationFunctions; + + // Member data for detecting when we need to force-recompose + // --------------------------------------------------------- + // Flag for tracking whether we need to compose. Initialized to false, but + // gets flipped to true if we detect that something has changed. + bool mForceCompositing; + + // Cached base value, so we can detect & force-recompose when it changes + // from one sample to the next. (nsSMILAnimationController copies this + // forward from the previous sample's compositor.) + nsAutoPtr mCachedBaseValue; +}; + +#endif // NS_SMILCOMPOSITOR_H_ diff --git a/dom/smil/nsSMILCompositorTable.h b/dom/smil/nsSMILCompositorTable.h new file mode 100644 index 000000000..b35d50117 --- /dev/null +++ b/dom/smil/nsSMILCompositorTable.h @@ -0,0 +1,23 @@ +/* -*- 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/. */ + +#ifndef NS_SMILCOMPOSITORTABLE_H_ +#define NS_SMILCOMPOSITORTABLE_H_ + +#include "nsTHashtable.h" + +//---------------------------------------------------------------------- +// nsSMILCompositorTable : A hashmap of nsSMILCompositors +// +// This is just a forward-declaration because it is included in +// nsSMILAnimationController which is used in nsDocument. We don't want to +// expose all of nsSMILCompositor or otherwise any changes to it will mean the +// whole world will need to be rebuilt. + +class nsSMILCompositor; +typedef nsTHashtable nsSMILCompositorTable; + +#endif // NS_SMILCOMPOSITORTABLE_H_ diff --git a/dom/smil/nsSMILFloatType.cpp b/dom/smil/nsSMILFloatType.cpp new file mode 100644 index 000000000..d3e298043 --- /dev/null +++ b/dom/smil/nsSMILFloatType.cpp @@ -0,0 +1,92 @@ +/* -*- 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 "nsSMILFloatType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include + +void +nsSMILFloatType::Init(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mDouble = 0.0; + aValue.mType = this; +} + +void +nsSMILFloatType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mDouble = 0.0; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +nsSMILFloatType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mDouble = aSrc.mU.mDouble; + return NS_OK; +} + +bool +nsSMILFloatType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mDouble == aRight.mU.mDouble; +} + +nsresult +nsSMILFloatType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + aDest.mU.mDouble += aValueToAdd.mU.mDouble * aCount; + return NS_OK; +} + +nsresult +nsSMILFloatType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + + const double &from = aFrom.mU.mDouble; + const double &to = aTo.mU.mDouble; + + aDistance = fabs(to - from); + + return NS_OK; +} + +nsresult +nsSMILFloatType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + + const double &startVal = aStartVal.mU.mDouble; + const double &endVal = aEndVal.mU.mDouble; + + aResult.mU.mDouble = (startVal + (endVal - startVal) * aUnitDistance); + + return NS_OK; +} diff --git a/dom/smil/nsSMILFloatType.h b/dom/smil/nsSMILFloatType.h new file mode 100644 index 000000000..fd57e4a77 --- /dev/null +++ b/dom/smil/nsSMILFloatType.h @@ -0,0 +1,47 @@ +/* -*- 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/. */ + +#ifndef NS_SMILFLOATTYPE_H_ +#define NS_SMILFLOATTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +class nsSMILFloatType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static nsSMILFloatType* + Singleton() + { + static nsSMILFloatType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr nsSMILFloatType() {} +}; + +#endif // NS_SMILFLOATTYPE_H_ diff --git a/dom/smil/nsSMILInstanceTime.cpp b/dom/smil/nsSMILInstanceTime.cpp new file mode 100644 index 000000000..f5d27fdee --- /dev/null +++ b/dom/smil/nsSMILInstanceTime.cpp @@ -0,0 +1,212 @@ +/* -*- 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 "nsSMILInstanceTime.h" +#include "nsSMILInterval.h" +#include "nsSMILTimeValueSpec.h" +#include "mozilla/AutoRestore.h" + +//---------------------------------------------------------------------- +// Implementation + +nsSMILInstanceTime::nsSMILInstanceTime(const nsSMILTimeValue& aTime, + nsSMILInstanceTimeSource aSource, + nsSMILTimeValueSpec* aCreator, + nsSMILInterval* aBaseInterval) + : mTime(aTime), + mFlags(0), + mVisited(false), + mFixedEndpointRefCnt(0), + mSerial(0), + mCreator(aCreator), + mBaseInterval(nullptr) // This will get set to aBaseInterval in a call to + // SetBaseInterval() at end of constructor +{ + switch (aSource) { + case SOURCE_NONE: + // No special flags + break; + + case SOURCE_DOM: + mFlags = kDynamic | kFromDOM; + break; + + case SOURCE_SYNCBASE: + mFlags = kMayUpdate; + break; + + case SOURCE_EVENT: + mFlags = kDynamic; + break; + } + + SetBaseInterval(aBaseInterval); +} + +nsSMILInstanceTime::~nsSMILInstanceTime() +{ + MOZ_ASSERT(!mBaseInterval, + "Destroying instance time without first calling Unlink()"); + MOZ_ASSERT(mFixedEndpointRefCnt == 0, + "Destroying instance time that is still used as the fixed " + "endpoint of an interval"); +} + +void +nsSMILInstanceTime::Unlink() +{ + RefPtr deathGrip(this); + if (mBaseInterval) { + mBaseInterval->RemoveDependentTime(*this); + mBaseInterval = nullptr; + } + mCreator = nullptr; +} + +void +nsSMILInstanceTime::HandleChangedInterval( + const nsSMILTimeContainer* aSrcContainer, + bool aBeginObjectChanged, + bool aEndObjectChanged) +{ + // It's possible a sequence of notifications might cause our base interval to + // be updated and then deleted. Furthermore, the delete might happen whilst + // we're still in the queue to be notified of the change. In any case, if we + // don't have a base interval, just ignore the change. + if (!mBaseInterval) + return; + + MOZ_ASSERT(mCreator, "Base interval is set but creator is not."); + + if (mVisited) { + // Break the cycle here + Unlink(); + return; + } + + bool objectChanged = mCreator->DependsOnBegin() ? aBeginObjectChanged : + aEndObjectChanged; + + RefPtr deathGrip(this); + mozilla::AutoRestore setVisited(mVisited); + mVisited = true; + + mCreator->HandleChangedInstanceTime(*GetBaseTime(), aSrcContainer, *this, + objectChanged); +} + +void +nsSMILInstanceTime::HandleDeletedInterval() +{ + MOZ_ASSERT(mBaseInterval, + "Got call to HandleDeletedInterval on an independent instance " + "time"); + MOZ_ASSERT(mCreator, "Base interval is set but creator is not"); + + mBaseInterval = nullptr; + mFlags &= ~kMayUpdate; // Can't update without a base interval + + RefPtr deathGrip(this); + mCreator->HandleDeletedInstanceTime(*this); + mCreator = nullptr; +} + +void +nsSMILInstanceTime::HandleFilteredInterval() +{ + MOZ_ASSERT(mBaseInterval, + "Got call to HandleFilteredInterval on an independent instance " + "time"); + + mBaseInterval = nullptr; + mFlags &= ~kMayUpdate; // Can't update without a base interval + mCreator = nullptr; +} + +bool +nsSMILInstanceTime::ShouldPreserve() const +{ + return mFixedEndpointRefCnt > 0 || (mFlags & kWasDynamicEndpoint); +} + +void +nsSMILInstanceTime::UnmarkShouldPreserve() +{ + mFlags &= ~kWasDynamicEndpoint; +} + +void +nsSMILInstanceTime::AddRefFixedEndpoint() +{ + MOZ_ASSERT(mFixedEndpointRefCnt < UINT16_MAX, + "Fixed endpoint reference count upper limit reached"); + ++mFixedEndpointRefCnt; + mFlags &= ~kMayUpdate; // Once fixed, always fixed +} + +void +nsSMILInstanceTime::ReleaseFixedEndpoint() +{ + MOZ_ASSERT(mFixedEndpointRefCnt > 0, "Duplicate release"); + --mFixedEndpointRefCnt; + if (mFixedEndpointRefCnt == 0 && IsDynamic()) { + mFlags |= kWasDynamicEndpoint; + } +} + +bool +nsSMILInstanceTime::IsDependentOn(const nsSMILInstanceTime& aOther) const +{ + if (mVisited) + return false; + + const nsSMILInstanceTime* myBaseTime = GetBaseTime(); + if (!myBaseTime) + return false; + + if (myBaseTime == &aOther) + return true; + + mozilla::AutoRestore setVisited(mVisited); + mVisited = true; + return myBaseTime->IsDependentOn(aOther); +} + +const nsSMILInstanceTime* +nsSMILInstanceTime::GetBaseTime() const +{ + if (!mBaseInterval) { + return nullptr; + } + + MOZ_ASSERT(mCreator, "Base interval is set but there is no creator."); + if (!mCreator) { + return nullptr; + } + + return mCreator->DependsOnBegin() ? mBaseInterval->Begin() : + mBaseInterval->End(); +} + +void +nsSMILInstanceTime::SetBaseInterval(nsSMILInterval* aBaseInterval) +{ + MOZ_ASSERT(!mBaseInterval, + "Attempting to reassociate an instance time with a different " + "interval."); + + if (aBaseInterval) { + MOZ_ASSERT(mCreator, + "Attempting to create a dependent instance time without " + "reference to the creating nsSMILTimeValueSpec object."); + if (!mCreator) + return; + + aBaseInterval->AddDependentTime(*this); + } + + mBaseInterval = aBaseInterval; +} diff --git a/dom/smil/nsSMILInstanceTime.h b/dom/smil/nsSMILInstanceTime.h new file mode 100644 index 000000000..d5a5807e3 --- /dev/null +++ b/dom/smil/nsSMILInstanceTime.h @@ -0,0 +1,166 @@ +/* -*- 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/. */ + +#ifndef NS_SMILINSTANCETIME_H_ +#define NS_SMILINSTANCETIME_H_ + +#include "nsSMILTimeValue.h" + +class nsSMILInterval; +class nsSMILTimeContainer; +class nsSMILTimeValueSpec; + +//---------------------------------------------------------------------- +// nsSMILInstanceTime +// +// An instant in document simple time that may be used in creating a new +// interval. +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in nsSMILTimeValue.h +// +// These objects are owned by an nsSMILTimedElement but MAY also be referenced +// by: +// +// a) nsSMILIntervals that belong to the same nsSMILTimedElement and which refer +// to the nsSMILInstanceTimes which form the interval endpoints; and/or +// b) nsSMILIntervals that belong to other nsSMILTimedElements but which need to +// update dependent instance times when they change or are deleted. +// E.g. for begin='a.begin', 'a' needs to inform dependent +// nsSMILInstanceTimes if its begin time changes. This notification is +// performed by the nsSMILInterval. + +class nsSMILInstanceTime final +{ +public: + // Instance time source. Times generated by events, syncbase relationships, + // and DOM calls behave differently in some circumstances such as when a timed + // element is reset. + enum nsSMILInstanceTimeSource { + // No particularly significant source, e.g. offset time, 'indefinite' + SOURCE_NONE, + // Generated by a DOM call such as beginElement + SOURCE_DOM, + // Generated by a syncbase relationship + SOURCE_SYNCBASE, + // Generated by an event + SOURCE_EVENT + }; + + explicit nsSMILInstanceTime(const nsSMILTimeValue& aTime, + nsSMILInstanceTimeSource aSource = SOURCE_NONE, + nsSMILTimeValueSpec* aCreator = nullptr, + nsSMILInterval* aBaseInterval = nullptr); + + void Unlink(); + void HandleChangedInterval(const nsSMILTimeContainer* aSrcContainer, + bool aBeginObjectChanged, + bool aEndObjectChanged); + void HandleDeletedInterval(); + void HandleFilteredInterval(); + + const nsSMILTimeValue& Time() const { return mTime; } + const nsSMILTimeValueSpec* GetCreator() const { return mCreator; } + + bool IsDynamic() const { return !!(mFlags & kDynamic); } + bool IsFixedTime() const { return !(mFlags & kMayUpdate); } + bool FromDOM() const { return !!(mFlags & kFromDOM); } + + bool ShouldPreserve() const; + void UnmarkShouldPreserve(); + + void AddRefFixedEndpoint(); + void ReleaseFixedEndpoint(); + + void DependentUpdate(const nsSMILTimeValue& aNewTime) + { + MOZ_ASSERT(!IsFixedTime(), + "Updating an instance time that is not expected to be updated"); + mTime = aNewTime; + } + + bool IsDependent() const { return !!mBaseInterval; } + bool IsDependentOn(const nsSMILInstanceTime& aOther) const; + const nsSMILInterval* GetBaseInterval() const { return mBaseInterval; } + const nsSMILInstanceTime* GetBaseTime() const; + + bool SameTimeAndBase(const nsSMILInstanceTime& aOther) const + { + return mTime == aOther.mTime && GetBaseTime() == aOther.GetBaseTime(); + } + + // Get and set a serial number which may be used by a containing class to + // control the sort order of otherwise similar instance times. + uint32_t Serial() const { return mSerial; } + void SetSerial(uint32_t aIndex) { mSerial = aIndex; } + + NS_INLINE_DECL_REFCOUNTING(nsSMILInstanceTime) + +private: + // Private destructor, to discourage deletion outside of Release(): + ~nsSMILInstanceTime(); + + void SetBaseInterval(nsSMILInterval* aBaseInterval); + + nsSMILTimeValue mTime; + + // Internal flags used to represent the behaviour of different instance times + enum { + // Indicates that this instance time was generated by an event or a DOM + // call. Such instance times require special handling when (i) the owning + // element is reset, (ii) when they are to be added as a new end instance + // times (as per SMIL's event sensitivity contraints), and (iii) when + // a backwards seek is performed and the timing model is reconstructed. + kDynamic = 1, + + // Indicates that this instance time is referred to by an + // nsSMILTimeValueSpec and as such may be updated. Such instance time should + // not be filtered out by the nsSMILTimedElement even if they appear to be + // in the past as they may be updated to a future time. + kMayUpdate = 2, + + // Indicates that this instance time was generated from the DOM as opposed + // to an nsSMILTimeValueSpec. When a 'begin' or 'end' attribute is set or + // reset we should clear all the instance times that have been generated by + // that attribute (and hence an nsSMILTimeValueSpec), but not those from the + // DOM. + kFromDOM = 4, + + // Indicates that this instance time was used as the endpoint of an interval + // that has been filtered or removed. However, since it is a dynamic time it + // should be preserved and not filtered. + kWasDynamicEndpoint = 8 + }; + uint8_t mFlags; // Combination of kDynamic, kMayUpdate, etc. + mutable bool mVisited; // Cycle tracking + + // Additional reference count to determine if this instance time is currently + // used as a fixed endpoint in any intervals. Instance times that are used in + // this way should not be removed when the owning nsSMILTimedElement removes + // instance times in response to a restart or in an attempt to free up memory + // by filtering out old instance times. + // + // Instance times are only shared in a few cases, namely: + // a) early ends, + // b) zero-duration intervals, + // c) momentarily whilst establishing new intervals and updating the current + // interval, and + // d) trimmed intervals + // Hence the limited range of a uint16_t should be more than adequate. + uint16_t mFixedEndpointRefCnt; + + uint32_t mSerial; // A serial number used by the containing class to + // specify the sort order for instance times with the + // same mTime. + + nsSMILTimeValueSpec* mCreator; // The nsSMILTimeValueSpec object that created + // us. (currently only needed for syncbase + // instance times.) + nsSMILInterval* mBaseInterval; // Interval from which this time is derived + // (only used for syncbase instance times) +}; + +#endif // NS_SMILINSTANCETIME_H_ diff --git a/dom/smil/nsSMILInterval.cpp b/dom/smil/nsSMILInterval.cpp new file mode 100644 index 000000000..956efd626 --- /dev/null +++ b/dom/smil/nsSMILInterval.cpp @@ -0,0 +1,170 @@ +/* -*- 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 "nsSMILInterval.h" + +nsSMILInterval::nsSMILInterval() +: + mBeginFixed(false), + mEndFixed(false) +{ +} + +nsSMILInterval::nsSMILInterval(const nsSMILInterval& aOther) +: + mBegin(aOther.mBegin), + mEnd(aOther.mEnd), + mBeginFixed(false), + mEndFixed(false) +{ + MOZ_ASSERT(aOther.mDependentTimes.IsEmpty(), + "Attempt to copy-construct an interval with dependent times; this " + "will lead to instance times being shared between intervals."); + + // For the time being we don't allow intervals with fixed endpoints to be + // copied since we only ever copy-construct to establish a new current + // interval. If we ever need to copy historical intervals we may need to move + // the ReleaseFixedEndpoint calls from Unlink to the dtor. + MOZ_ASSERT(!aOther.mBeginFixed && !aOther.mEndFixed, + "Attempt to copy-construct an interval with fixed endpoints"); +} + +nsSMILInterval::~nsSMILInterval() +{ + MOZ_ASSERT(mDependentTimes.IsEmpty(), + "Destroying interval without disassociating dependent instance " + "times. Unlink was not called"); +} + +void +nsSMILInterval::Unlink(bool aFiltered) +{ + for (int32_t i = mDependentTimes.Length() - 1; i >= 0; --i) { + if (aFiltered) { + mDependentTimes[i]->HandleFilteredInterval(); + } else { + mDependentTimes[i]->HandleDeletedInterval(); + } + } + mDependentTimes.Clear(); + if (mBegin && mBeginFixed) { + mBegin->ReleaseFixedEndpoint(); + } + mBegin = nullptr; + if (mEnd && mEndFixed) { + mEnd->ReleaseFixedEndpoint(); + } + mEnd = nullptr; +} + +nsSMILInstanceTime* +nsSMILInterval::Begin() +{ + MOZ_ASSERT(mBegin && mEnd, + "Requesting Begin() on un-initialized interval."); + return mBegin; +} + +nsSMILInstanceTime* +nsSMILInterval::End() +{ + MOZ_ASSERT(mBegin && mEnd, + "Requesting End() on un-initialized interval."); + return mEnd; +} + +void +nsSMILInterval::SetBegin(nsSMILInstanceTime& aBegin) +{ + MOZ_ASSERT(aBegin.Time().IsDefinite(), + "Attempt to set unresolved or indefinite begin time on interval"); + MOZ_ASSERT(!mBeginFixed, + "Attempt to set begin time but the begin point is fixed"); + // Check that we're not making an instance time dependent on itself. Such an + // arrangement does not make intuitive sense and should be detected when + // creating or updating intervals. + MOZ_ASSERT(!mBegin || aBegin.GetBaseTime() != mBegin, + "Attempt to make self-dependent instance time"); + + mBegin = &aBegin; +} + +void +nsSMILInterval::SetEnd(nsSMILInstanceTime& aEnd) +{ + MOZ_ASSERT(!mEndFixed, + "Attempt to set end time but the end point is fixed"); + // As with SetBegin, check we're not making an instance time dependent on + // itself. + MOZ_ASSERT(!mEnd || aEnd.GetBaseTime() != mEnd, + "Attempting to make self-dependent instance time"); + + mEnd = &aEnd; +} + +void +nsSMILInterval::FixBegin() +{ + MOZ_ASSERT(mBegin && mEnd, + "Fixing begin point on un-initialized interval"); + MOZ_ASSERT(!mBeginFixed, "Duplicate calls to FixBegin()"); + mBeginFixed = true; + mBegin->AddRefFixedEndpoint(); +} + +void +nsSMILInterval::FixEnd() +{ + MOZ_ASSERT(mBegin && mEnd, + "Fixing end point on un-initialized interval"); + MOZ_ASSERT(mBeginFixed, + "Fixing the end of an interval without a fixed begin"); + MOZ_ASSERT(!mEndFixed, "Duplicate calls to FixEnd()"); + mEndFixed = true; + mEnd->AddRefFixedEndpoint(); +} + +void +nsSMILInterval::AddDependentTime(nsSMILInstanceTime& aTime) +{ + RefPtr* inserted = + mDependentTimes.InsertElementSorted(&aTime); + if (!inserted) { + NS_WARNING("Insufficient memory to insert instance time."); + } +} + +void +nsSMILInterval::RemoveDependentTime(const nsSMILInstanceTime& aTime) +{ +#ifdef DEBUG + bool found = +#endif + mDependentTimes.RemoveElementSorted(&aTime); + MOZ_ASSERT(found, "Couldn't find instance time to delete."); +} + +void +nsSMILInterval::GetDependentTimes(InstanceTimeList& aTimes) +{ + aTimes = mDependentTimes; +} + +bool +nsSMILInterval::IsDependencyChainLink() const +{ + if (!mBegin || !mEnd) + return false; // Not yet initialised so it can't be part of a chain + + if (mDependentTimes.IsEmpty()) + return false; // No dependents, chain end + + // So we have dependents, but we're still only a link in the chain (as opposed + // to the end of the chain) if one of our endpoints is dependent on an + // interval other than ourselves. + return (mBegin->IsDependent() && mBegin->GetBaseInterval() != this) || + (mEnd->IsDependent() && mEnd->GetBaseInterval() != this); +} diff --git a/dom/smil/nsSMILInterval.h b/dom/smil/nsSMILInterval.h new file mode 100644 index 000000000..d30728821 --- /dev/null +++ b/dom/smil/nsSMILInterval.h @@ -0,0 +1,86 @@ +/* -*- 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/. */ + +#ifndef NS_SMILINTERVAL_H_ +#define NS_SMILINTERVAL_H_ + +#include "nsSMILInstanceTime.h" +#include "nsTArray.h" + +//---------------------------------------------------------------------- +// nsSMILInterval class +// +// A structure consisting of a begin and end time. The begin time must be +// resolved (i.e. not indefinite or unresolved). +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in nsSMILTimeValue.h + +class nsSMILInterval +{ +public: + nsSMILInterval(); + nsSMILInterval(const nsSMILInterval& aOther); + ~nsSMILInterval(); + void Unlink(bool aFiltered = false); + + const nsSMILInstanceTime* Begin() const + { + MOZ_ASSERT(mBegin && mEnd, + "Requesting Begin() on un-initialized instance time"); + return mBegin; + } + nsSMILInstanceTime* Begin(); + + const nsSMILInstanceTime* End() const + { + MOZ_ASSERT(mBegin && mEnd, + "Requesting End() on un-initialized instance time"); + return mEnd; + } + nsSMILInstanceTime* End(); + + void SetBegin(nsSMILInstanceTime& aBegin); + void SetEnd(nsSMILInstanceTime& aEnd); + void Set(nsSMILInstanceTime& aBegin, nsSMILInstanceTime& aEnd) + { + SetBegin(aBegin); + SetEnd(aEnd); + } + + void FixBegin(); + void FixEnd(); + + typedef nsTArray > InstanceTimeList; + + void AddDependentTime(nsSMILInstanceTime& aTime); + void RemoveDependentTime(const nsSMILInstanceTime& aTime); + void GetDependentTimes(InstanceTimeList& aTimes); + + // Cue for assessing if this interval can be filtered + bool IsDependencyChainLink() const; + +private: + RefPtr mBegin; + RefPtr mEnd; + + // nsSMILInstanceTimes to notify when this interval is changed or deleted. + InstanceTimeList mDependentTimes; + + // Indicates if the end points of the interval are fixed or not. + // + // Note that this is not the same as having an end point whose TIME is fixed + // (i.e. nsSMILInstanceTime::IsFixed() returns true). This is because it is + // possible to have an end point with a fixed TIME and yet still update the + // end point to refer to a different nsSMILInstanceTime object. + // + // However, if mBegin/EndFixed is true, then BOTH the nsSMILInstanceTime + // OBJECT returned for that end point and its TIME value will not change. + bool mBeginFixed; + bool mEndFixed; +}; + +#endif // NS_SMILINTERVAL_H_ diff --git a/dom/smil/nsSMILKeySpline.cpp b/dom/smil/nsSMILKeySpline.cpp new file mode 100644 index 000000000..716437aab --- /dev/null +++ b/dom/smil/nsSMILKeySpline.cpp @@ -0,0 +1,151 @@ +/* -*- 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 "nsSMILKeySpline.h" +#include +#include + +#define NEWTON_ITERATIONS 4 +#define NEWTON_MIN_SLOPE 0.02 +#define SUBDIVISION_PRECISION 0.0000001 +#define SUBDIVISION_MAX_ITERATIONS 10 + +const double nsSMILKeySpline::kSampleStepSize = + 1.0 / double(kSplineTableSize - 1); + +void +nsSMILKeySpline::Init(double aX1, + double aY1, + double aX2, + double aY2) +{ + mX1 = aX1; + mY1 = aY1; + mX2 = aX2; + mY2 = aY2; + + if (mX1 != mY1 || mX2 != mY2) + CalcSampleValues(); +} + +double +nsSMILKeySpline::GetSplineValue(double aX) const +{ + if (mX1 == mY1 && mX2 == mY2) + return aX; + + return CalcBezier(GetTForX(aX), mY1, mY2); +} + +void +nsSMILKeySpline::GetSplineDerivativeValues(double aX, double& aDX, double& aDY) const +{ + double t = GetTForX(aX); + aDX = GetSlope(t, mX1, mX2); + aDY = GetSlope(t, mY1, mY2); +} + +void +nsSMILKeySpline::CalcSampleValues() +{ + for (uint32_t i = 0; i < kSplineTableSize; ++i) { + mSampleValues[i] = CalcBezier(double(i) * kSampleStepSize, mX1, mX2); + } +} + +/*static*/ double +nsSMILKeySpline::CalcBezier(double aT, + double aA1, + double aA2) +{ + // use Horner's scheme to evaluate the Bezier polynomial + return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT; +} + +/*static*/ double +nsSMILKeySpline::GetSlope(double aT, + double aA1, + double aA2) +{ + return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1); +} + +double +nsSMILKeySpline::GetTForX(double aX) const +{ + // Early return when aX == 1.0 to avoid floating-point inaccuracies. + if (aX == 1.0) { + return 1.0; + } + // Find interval where t lies + double intervalStart = 0.0; + const double* currentSample = &mSampleValues[1]; + const double* const lastSample = &mSampleValues[kSplineTableSize - 1]; + for (; currentSample != lastSample && *currentSample <= aX; + ++currentSample) { + intervalStart += kSampleStepSize; + } + --currentSample; // t now lies between *currentSample and *currentSample+1 + + // Interpolate to provide an initial guess for t + double dist = (aX - *currentSample) / + (*(currentSample+1) - *currentSample); + double guessForT = intervalStart + dist * kSampleStepSize; + + // Check the slope to see what strategy to use. If the slope is too small + // Newton-Raphson iteration won't converge on a root so we use bisection + // instead. + double initialSlope = GetSlope(guessForT, mX1, mX2); + if (initialSlope >= NEWTON_MIN_SLOPE) { + return NewtonRaphsonIterate(aX, guessForT); + } else if (initialSlope == 0.0) { + return guessForT; + } else { + return BinarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize); + } +} + +double +nsSMILKeySpline::NewtonRaphsonIterate(double aX, double aGuessT) const +{ + // Refine guess with Newton-Raphson iteration + for (uint32_t i = 0; i < NEWTON_ITERATIONS; ++i) { + // We're trying to find where f(t) = aX, + // so we're actually looking for a root for: CalcBezier(t) - aX + double currentX = CalcBezier(aGuessT, mX1, mX2) - aX; + double currentSlope = GetSlope(aGuessT, mX1, mX2); + + if (currentSlope == 0.0) + return aGuessT; + + aGuessT -= currentX / currentSlope; + } + + return aGuessT; +} + +double +nsSMILKeySpline::BinarySubdivide(double aX, double aA, double aB) const +{ + double currentX; + double currentT; + uint32_t i = 0; + + do + { + currentT = aA + (aB - aA) / 2.0; + currentX = CalcBezier(currentT, mX1, mX2) - aX; + + if (currentX > 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (fabs(currentX) > SUBDIVISION_PRECISION + && ++i < SUBDIVISION_MAX_ITERATIONS); + + return currentT; +} diff --git a/dom/smil/nsSMILKeySpline.h b/dom/smil/nsSMILKeySpline.h new file mode 100644 index 000000000..36c14fec1 --- /dev/null +++ b/dom/smil/nsSMILKeySpline.h @@ -0,0 +1,122 @@ +/* -*- 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/. */ + +#ifndef NS_SMILKEYSPLINE_H_ +#define NS_SMILKEYSPLINE_H_ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/PodOperations.h" + +/** + * Utility class to provide scaling defined in a keySplines element. + */ +class nsSMILKeySpline +{ +public: + nsSMILKeySpline() { /* caller must call Init later */ } + + /** + * Creates a new key spline control point description. + * + * aX1, etc. are the x1, y1, x2, y2 cubic Bezier control points as defined by + * SMILANIM 3.2.3. They must each be in the range 0.0 <= x <= 1.0 + */ + nsSMILKeySpline(double aX1, double aY1, + double aX2, double aY2) + { + Init(aX1, aY1, aX2, aY2); + } + + double X1() const { return mX1; } + double Y1() const { return mY1; } + double X2() const { return mX2; } + double Y2() const { return mY2; } + + void Init(double aX1, double aY1, + double aX2, double aY2); + + /** + * Gets the output (y) value for an input (x). + * + * @param aX The input x value. A floating-point number between 0 and + * 1 (inclusive). + */ + double GetSplineValue(double aX) const; + + void GetSplineDerivativeValues(double aX, double& aDX, double& aDY) const; + + bool operator==(const nsSMILKeySpline& aOther) const { + return mX1 == aOther.mX1 && + mY1 == aOther.mY1 && + mX2 == aOther.mX2 && + mY2 == aOther.mY2; + } + bool operator!=(const nsSMILKeySpline& aOther) const { + return !(*this == aOther); + } + int32_t Compare(const nsSMILKeySpline& aRhs) const { + if (mX1 != aRhs.mX1) return mX1 < aRhs.mX1 ? -1 : 1; + if (mY1 != aRhs.mY1) return mY1 < aRhs.mY1 ? -1 : 1; + if (mX2 != aRhs.mX2) return mX2 < aRhs.mX2 ? -1 : 1; + if (mY2 != aRhs.mY2) return mY2 < aRhs.mY2 ? -1 : 1; + return 0; + } + +private: + void + CalcSampleValues(); + + /** + * Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. + */ + static double + CalcBezier(double aT, double aA1, double aA2); + + /** + * Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. + */ + static double + GetSlope(double aT, double aA1, double aA2); + + double + GetTForX(double aX) const; + + double + NewtonRaphsonIterate(double aX, double aGuessT) const; + + double + BinarySubdivide(double aX, double aA, double aB) const; + + static double + A(double aA1, double aA2) + { + return 1.0 - 3.0 * aA2 + 3.0 * aA1; + } + + static double + B(double aA1, double aA2) + { + return 3.0 * aA2 - 6.0 * aA1; + } + + static double + C(double aA1) + { + return 3.0 * aA1; + } + + double mX1; + double mY1; + double mX2; + double mY2; + + enum { kSplineTableSize = 11 }; + double mSampleValues[kSplineTableSize]; + + static const double kSampleStepSize; +}; + +#endif // NS_SMILKEYSPLINE_H_ diff --git a/dom/smil/nsSMILMappedAttribute.cpp b/dom/smil/nsSMILMappedAttribute.cpp new file mode 100644 index 000000000..b43469fac --- /dev/null +++ b/dom/smil/nsSMILMappedAttribute.cpp @@ -0,0 +1,150 @@ +/* -*- 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/. */ + +/* representation of a SMIL-animatable mapped attribute on an element */ +#include "nsSMILMappedAttribute.h" +#include "nsContentUtils.h" +#include "nsError.h" // For NS_PROPTABLE_PROP_OVERWRITTEN +#include "nsSMILValue.h" +#include "nsSMILCSSValueType.h" +#include "nsIDocument.h" +#include "nsIPresShell.h" +#include "nsCSSProps.h" +#include "mozilla/dom/Element.h" + +// Callback function, for freeing string buffers stored in property table +static void +ReleaseStringBufferPropertyValue(void* aObject, /* unused */ + nsIAtom* aPropertyName, /* unused */ + void* aPropertyValue, + void* aData /* unused */) +{ + nsStringBuffer* buf = static_cast(aPropertyValue); + buf->Release(); +} + + +nsresult +nsSMILMappedAttribute::ValueFromString(const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const +{ + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + nsSMILCSSValueType::ValueFromString(mPropID, mElement, aStr, aValue, + &aPreventCachingOfSandwich); + return aValue.IsNull() ? NS_ERROR_FAILURE : NS_OK; +} + +nsSMILValue +nsSMILMappedAttribute::GetBaseValue() const +{ + nsAutoString baseStringValue; + RefPtr attrName = GetAttrNameAtom(); + bool success = mElement->GetAttr(kNameSpaceID_None, attrName, + baseStringValue); + nsSMILValue baseValue; + if (success) { + // For base values, we don't need to worry whether the value returned is + // context-sensitive or not since the compositor will take care of comparing + // the returned (computed) base value and its cached value and determining + // if an update is required or not. + nsSMILCSSValueType::ValueFromString(mPropID, mElement, + baseStringValue, baseValue, nullptr); + } else { + // Attribute is unset -- use computed value. + // FIRST: Temporarily clear animated value, to make sure it doesn't pollute + // the computed value. (We want base value, _without_ animations applied.) + void* buf = mElement->UnsetProperty(SMIL_MAPPED_ATTR_ANIMVAL, + attrName, nullptr); + FlushChangesToTargetAttr(); + + // SECOND: we use nsSMILCSSProperty::GetBaseValue to look up the property's + // computed value. NOTE: This call will temporarily clear the SMIL + // override-style for the corresponding CSS property on our target element. + // This prevents any animations that target the CSS property from affecting + // animations that target the mapped attribute. + baseValue = nsSMILCSSProperty::GetBaseValue(); + + // FINALLY: If we originally had an animated value set, then set it again. + if (buf) { + mElement->SetProperty(SMIL_MAPPED_ATTR_ANIMVAL, attrName, buf, + ReleaseStringBufferPropertyValue); + FlushChangesToTargetAttr(); + } + } + return baseValue; +} + +nsresult +nsSMILMappedAttribute::SetAnimValue(const nsSMILValue& aValue) +{ + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + // Convert nsSMILValue to string + nsAutoString valStr; + if (!nsSMILCSSValueType::ValueToString(aValue, valStr)) { + NS_WARNING("Failed to convert nsSMILValue for mapped attr into a string"); + return NS_ERROR_FAILURE; + } + + RefPtr attrName = GetAttrNameAtom(); + nsStringBuffer* oldValStrBuf = static_cast + (mElement->GetProperty(SMIL_MAPPED_ATTR_ANIMVAL, attrName)); + if (oldValStrBuf) { + nsString oldValStr; + nsContentUtils::PopulateStringFromStringBuffer(oldValStrBuf, oldValStr); + if (valStr.Equals(oldValStr)) { + // New animated value is the same as the old; nothing to do. + return NS_OK; + } + } + + // Set the string as this mapped attribute's animated value. + nsStringBuffer* valStrBuf = + nsCSSValue::BufferFromString(nsString(valStr)).take(); + nsresult rv = mElement->SetProperty(SMIL_MAPPED_ATTR_ANIMVAL, + attrName, valStrBuf, + ReleaseStringBufferPropertyValue); + if (rv == NS_PROPTABLE_PROP_OVERWRITTEN) { + rv = NS_OK; + } + FlushChangesToTargetAttr(); + + return rv; +} + +void +nsSMILMappedAttribute::ClearAnimValue() +{ + RefPtr attrName = GetAttrNameAtom(); + mElement->DeleteProperty(SMIL_MAPPED_ATTR_ANIMVAL, attrName); + FlushChangesToTargetAttr(); +} + +void +nsSMILMappedAttribute::FlushChangesToTargetAttr() const +{ + // Clear animated content-style-rule + mElement->DeleteProperty(SMIL_MAPPED_ATTR_ANIMVAL, + SMIL_MAPPED_ATTR_STYLERULE_ATOM); + nsIDocument* doc = mElement->GetUncomposedDoc(); + + // Request animation restyle + if (doc) { + nsIPresShell* shell = doc->GetShell(); + if (shell) { + shell->RestyleForAnimation(mElement, eRestyle_Self); + } + } +} + +already_AddRefed +nsSMILMappedAttribute::GetAttrNameAtom() const +{ + return NS_Atomize(nsCSSProps::GetStringValue(mPropID)); +} diff --git a/dom/smil/nsSMILMappedAttribute.h b/dom/smil/nsSMILMappedAttribute.h new file mode 100644 index 000000000..212b65c92 --- /dev/null +++ b/dom/smil/nsSMILMappedAttribute.h @@ -0,0 +1,56 @@ +/* -*- 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/. */ + +/* representation of a SMIL-animatable mapped attribute on an element */ + +#ifndef NS_SMILMAPPEDATTRIBUTE_H_ +#define NS_SMILMAPPEDATTRIBUTE_H_ + +#include "mozilla/Attributes.h" +#include "nsSMILCSSProperty.h" + +/* We'll use the empty-string atom |nsGkAtoms::_empty| as the key for storing + * an element's animated content style rule in its Property Table, under the + * property-category SMIL_MAPPED_ATTR_ANIMVAL. Everything else stored in that + * category is keyed off of the XML attribute name, so the empty string is a + * good "reserved" key to use for storing the style rule (since XML attributes + * all have nonempty names). + */ +#define SMIL_MAPPED_ATTR_STYLERULE_ATOM nsGkAtoms::_empty + +/** + * nsSMILMappedAttribute: Implements the nsISMILAttr interface for SMIL + * animations whose targets are attributes that map to CSS properties. An + * instance of this class represents a particular animation-targeted mapped + * attribute on a particular element. + */ +class nsSMILMappedAttribute : public nsSMILCSSProperty { +public: + /** + * Constructs a new nsSMILMappedAttribute. + * + * @param aPropID The CSS property for the mapped attribute we're + * interested in animating. + * @param aElement The element whose attribute is being animated. + */ + nsSMILMappedAttribute(nsCSSPropertyID aPropID, mozilla::dom::Element* aElement) : + nsSMILCSSProperty(aPropID, aElement) {} + + // nsISMILAttr methods + virtual nsresult ValueFromString(const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const override; + virtual nsSMILValue GetBaseValue() const override; + virtual nsresult SetAnimValue(const nsSMILValue& aValue) override; + virtual void ClearAnimValue() override; + +protected: + // Helper Methods + void FlushChangesToTargetAttr() const; + already_AddRefed GetAttrNameAtom() const; +}; +#endif // NS_SMILMAPPEDATTRIBUTE_H_ diff --git a/dom/smil/nsSMILMilestone.h b/dom/smil/nsSMILMilestone.h new file mode 100644 index 000000000..e5f330114 --- /dev/null +++ b/dom/smil/nsSMILMilestone.h @@ -0,0 +1,79 @@ +/* -*- 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/. */ + +#ifndef NS_SMILMILESTONE_H_ +#define NS_SMILMILESTONE_H_ + +/* + * A significant moment in an nsSMILTimedElement's lifetime where a sample is + * required. + * + * Animations register the next milestone in their lifetime with the time + * container that they belong to. When the animation controller goes to run + * a sample it first visits all the animations that have a registered milestone + * in order of their milestone times. This allows interdependencies between + * animations to be correctly resolved and events to fire in the proper order. + * + * A distinction is made between a milestone representing the end of an interval + * and any other milestone. This is because if animation A ends at time t, and + * animation B begins at the same time t (or has some other significant moment + * such as firing a repeat event), SMIL's endpoint-exclusive timing model + * implies that the interval end occurs first. In fact, interval ends can be + * thought of as ending an infinitesimally small time before t. Therefore, + * A should be sampled before B. + * + * Furthermore, this distinction between sampling the end of an interval and + * a regular sample is used within the timing model (specifically in + * nsSMILTimedElement) to ensure that all intervals ending at time t are sampled + * before any new intervals are entered so that we have a fully up-to-date set + * of instance times available before committing to a new interval. Once an + * interval is entered, the begin time is fixed. + */ +class nsSMILMilestone +{ +public: + nsSMILMilestone(nsSMILTime aTime, bool aIsEnd) + : mTime(aTime), mIsEnd(aIsEnd) + { } + + nsSMILMilestone() + : mTime(0), mIsEnd(false) + { } + + bool operator==(const nsSMILMilestone& aOther) const + { + return mTime == aOther.mTime && mIsEnd == aOther.mIsEnd; + } + + bool operator!=(const nsSMILMilestone& aOther) const + { + return !(*this == aOther); + } + + bool operator<(const nsSMILMilestone& aOther) const + { + // Earlier times sort first, and for equal times end milestones sort first + return mTime < aOther.mTime || + (mTime == aOther.mTime && mIsEnd && !aOther.mIsEnd); + } + + bool operator<=(const nsSMILMilestone& aOther) const + { + return *this == aOther || *this < aOther; + } + + bool operator>=(const nsSMILMilestone& aOther) const + { + return !(*this < aOther); + } + + nsSMILTime mTime; // The milestone time. This may be in container time or + // parent container time depending on where it is used. + bool mIsEnd; // true if this milestone corresponds to an interval + // end, false otherwise. +}; + +#endif // NS_SMILMILESTONE_H_ diff --git a/dom/smil/nsSMILNullType.cpp b/dom/smil/nsSMILNullType.cpp new file mode 100644 index 000000000..b51c135af --- /dev/null +++ b/dom/smil/nsSMILNullType.cpp @@ -0,0 +1,56 @@ +/* -*- 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 "nsSMILNullType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" + +nsresult +nsSMILNullType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aSrc.mType == this, "Unexpected source type"); + aDest.mU = aSrc.mU; + aDest.mType = Singleton(); + return NS_OK; +} + +bool +nsSMILNullType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return true; // All null-typed values are equivalent. +} + +nsresult +nsSMILNullType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_NOTREACHED("Adding NULL type"); + return NS_ERROR_FAILURE; +} + +nsresult +nsSMILNullType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_NOTREACHED("Computing distance for NULL type"); + return NS_ERROR_FAILURE; +} + +nsresult +nsSMILNullType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_NOTREACHED("Interpolating NULL type"); + return NS_ERROR_FAILURE; +} diff --git a/dom/smil/nsSMILNullType.h b/dom/smil/nsSMILNullType.h new file mode 100644 index 000000000..d21610ff4 --- /dev/null +++ b/dom/smil/nsSMILNullType.h @@ -0,0 +1,50 @@ +/* -*- 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/. */ + +#ifndef NS_SMILNULLTYPE_H_ +#define NS_SMILNULLTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +class nsSMILNullType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static nsSMILNullType* + Singleton() + { + static nsSMILNullType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override {} + virtual void Destroy(nsSMILValue& aValue) const override {} + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + + // The remaining methods should never be called, so although they're very + // simple they don't need to be inline. + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr nsSMILNullType() {} +}; + +#endif // NS_SMILNULLTYPE_H_ diff --git a/dom/smil/nsSMILParserUtils.cpp b/dom/smil/nsSMILParserUtils.cpp new file mode 100644 index 000000000..9174bdd4a --- /dev/null +++ b/dom/smil/nsSMILParserUtils.cpp @@ -0,0 +1,730 @@ +/* -*- 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 "nsSMILParserUtils.h" +#include "nsSMILKeySpline.h" +#include "nsISMILAttr.h" +#include "nsSMILValue.h" +#include "nsSMILTimeValue.h" +#include "nsSMILTimeValueSpecParams.h" +#include "nsSMILTypes.h" +#include "nsSMILRepeatCount.h" +#include "nsContentUtils.h" +#include "nsCharSeparatedTokenizer.h" +#include "SVGContentUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; +//------------------------------------------------------------------------------ +// Helper functions and Constants + +namespace { + +const uint32_t MSEC_PER_SEC = 1000; +const uint32_t MSEC_PER_MIN = 1000 * 60; +const uint32_t MSEC_PER_HOUR = 1000 * 60 * 60; + +#define ACCESSKEY_PREFIX_LC NS_LITERAL_STRING("accesskey(") // SMIL2+ +#define ACCESSKEY_PREFIX_CC NS_LITERAL_STRING("accessKey(") // SVG/SMIL ANIM +#define REPEAT_PREFIX NS_LITERAL_STRING("repeat(") +#define WALLCLOCK_PREFIX NS_LITERAL_STRING("wallclock(") + +inline bool +SkipWhitespace(RangedPtr& aIter, + const RangedPtr& aEnd) +{ + while (aIter != aEnd) { + if (!IsSVGWhitespace(*aIter)) { + return true; + } + ++aIter; + } + return false; +} + +inline bool +ParseColon(RangedPtr& aIter, + const RangedPtr& aEnd) +{ + if (aIter == aEnd || *aIter != ':') { + return false; + } + ++aIter; + return true; +} + +/* + * Exactly two digits in the range 00 - 59 are expected. + */ +bool +ParseSecondsOrMinutes(RangedPtr& aIter, + const RangedPtr& aEnd, + uint32_t& aValue) +{ + if (aIter == aEnd || !SVGContentUtils::IsDigit(*aIter)) { + return false; + } + + RangedPtr iter(aIter); + + if (++iter == aEnd || !SVGContentUtils::IsDigit(*iter)) { + return false; + } + + uint32_t value = 10 * SVGContentUtils::DecimalDigitValue(*aIter) + + SVGContentUtils::DecimalDigitValue(*iter); + if (value > 59) { + return false; + } + if (++iter != aEnd && SVGContentUtils::IsDigit(*iter)) { + return false; + } + + aValue = value; + aIter = iter; + return true; +} + +inline bool +ParseClockMetric(RangedPtr& aIter, + const RangedPtr& aEnd, + uint32_t& aMultiplier) +{ + if (aIter == aEnd) { + aMultiplier = MSEC_PER_SEC; + return true; + } + + switch (*aIter) { + case 'h': + if (++aIter == aEnd) { + aMultiplier = MSEC_PER_HOUR; + return true; + } + return false; + case 'm': + { + const nsAString& metric = Substring(aIter.get(), aEnd.get()); + if (metric.EqualsLiteral("min")) { + aMultiplier = MSEC_PER_MIN; + aIter = aEnd; + return true; + } + if (metric.EqualsLiteral("ms")) { + aMultiplier = 1; + aIter = aEnd; + return true; + } + } + return false; + case 's': + if (++aIter == aEnd) { + aMultiplier = MSEC_PER_SEC; + return true; + } + } + return false; +} + +/** + * See http://www.w3.org/TR/SVG/animate.html#ClockValueSyntax + */ +bool +ParseClockValue(RangedPtr& aIter, + const RangedPtr& aEnd, + nsSMILTimeValue* aResult) +{ + if (aIter == aEnd) { + return false; + } + + // TIMECOUNT_VALUE ::= Timecount ("." Fraction)? (Metric)? + // PARTIAL_CLOCK_VALUE ::= Minutes ":" Seconds ("." Fraction)? + // FULL_CLOCK_VALUE ::= Hours ":" Minutes ":" Seconds ("." Fraction)? + enum ClockType { + TIMECOUNT_VALUE, + PARTIAL_CLOCK_VALUE, + FULL_CLOCK_VALUE + }; + + int32_t clockType = TIMECOUNT_VALUE; + + RangedPtr iter(aIter); + + // Determine which type of clock value we have by counting the number + // of colons in the string. + do { + switch (*iter) { + case ':': + if (clockType == FULL_CLOCK_VALUE) { + return false; + } + ++clockType; + break; + case 'e': + case 'E': + case '-': + case '+': + // Exclude anything invalid (for clock values) + // that number parsing might otherwise allow. + return false; + } + ++iter; + } while (iter != aEnd); + + iter = aIter; + + int32_t hours = 0, timecount; + double fraction = 0.0; + uint32_t minutes, seconds, multiplier; + + switch (clockType) { + case FULL_CLOCK_VALUE: + if (!SVGContentUtils::ParseInteger(iter, aEnd, hours) || + !ParseColon(iter, aEnd)) { + return false; + } + MOZ_FALLTHROUGH; + case PARTIAL_CLOCK_VALUE: + if (!ParseSecondsOrMinutes(iter, aEnd, minutes) || + !ParseColon(iter, aEnd) || + !ParseSecondsOrMinutes(iter, aEnd, seconds)) { + return false; + } + if (iter != aEnd && + (*iter != '.' || + !SVGContentUtils::ParseNumber(iter, aEnd, fraction))) { + return false; + } + aResult->SetMillis(nsSMILTime(hours) * MSEC_PER_HOUR + + minutes * MSEC_PER_MIN + + seconds * MSEC_PER_SEC + + NS_round(fraction * MSEC_PER_SEC)); + aIter = iter; + return true; + case TIMECOUNT_VALUE: + if (!SVGContentUtils::ParseInteger(iter, aEnd, timecount)) { + return false; + } + if (iter != aEnd && *iter == '.' && + !SVGContentUtils::ParseNumber(iter, aEnd, fraction)) { + return false; + } + if (!ParseClockMetric(iter, aEnd, multiplier)) { + return false; + } + aResult->SetMillis(nsSMILTime(timecount) * multiplier + + NS_round(fraction * multiplier)); + aIter = iter; + return true; + } + + return false; +} + +bool +ParseOffsetValue(RangedPtr& aIter, + const RangedPtr& aEnd, + nsSMILTimeValue* aResult) +{ + RangedPtr iter(aIter); + + int32_t sign; + if (!SVGContentUtils::ParseOptionalSign(iter, aEnd, sign) || + !SkipWhitespace(iter, aEnd) || + !ParseClockValue(iter, aEnd, aResult)) { + return false; + } + if (sign == -1) { + aResult->SetMillis(-aResult->GetMillis()); + } + aIter = iter; + return true; +} + +bool +ParseOffsetValue(const nsAString& aSpec, + nsSMILTimeValue* aResult) +{ + RangedPtr iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + const RangedPtr end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + return ParseOffsetValue(iter, end, aResult) && iter == end; +} + +bool +ParseOptionalOffset(RangedPtr& aIter, + const RangedPtr& aEnd, + nsSMILTimeValue* aResult) +{ + if (aIter == aEnd) { + aResult->SetMillis(0L); + return true; + } + + return SkipWhitespace(aIter, aEnd) && + ParseOffsetValue(aIter, aEnd, aResult); +} + +bool +ParseAccessKey(const nsAString& aSpec, nsSMILTimeValueSpecParams& aResult) +{ + MOZ_ASSERT(StringBeginsWith(aSpec, ACCESSKEY_PREFIX_CC) || + StringBeginsWith(aSpec, ACCESSKEY_PREFIX_LC), + "Calling ParseAccessKey on non-accesskey-type spec"); + + nsSMILTimeValueSpecParams result; + result.mType = nsSMILTimeValueSpecParams::ACCESSKEY; + + MOZ_ASSERT(ACCESSKEY_PREFIX_LC.Length() == ACCESSKEY_PREFIX_CC.Length(), + "Case variations for accesskey prefix differ in length"); + + RangedPtr iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + iter += ACCESSKEY_PREFIX_LC.Length(); + + // Expecting at least + ')' + if (end - iter < 2) + return false; + + uint32_t c = *iter++; + + // Process 32-bit codepoints + if (NS_IS_HIGH_SURROGATE(c)) { + if (end - iter < 2) // Expecting at least low-surrogate + ')' + return false; + uint32_t lo = *iter++; + if (!NS_IS_LOW_SURROGATE(lo)) + return false; + c = SURROGATE_TO_UCS4(c, lo); + // XML 1.1 says that 0xFFFE and 0xFFFF are not valid characters + } else if (NS_IS_LOW_SURROGATE(c) || c == 0xFFFE || c == 0xFFFF) { + return false; + } + + result.mRepeatIterationOrAccessKey = c; + + if (*iter++ != ')') + return false; + + if (!ParseOptionalOffset(iter, end, &result.mOffset) || iter != end) { + return false; + } + aResult = result; + return true; +} + +void +MoveToNextToken(RangedPtr& aIter, + const RangedPtr& aEnd, + bool aBreakOnDot, + bool& aIsAnyCharEscaped) +{ + aIsAnyCharEscaped = false; + + bool isCurrentCharEscaped = false; + + while (aIter != aEnd && !IsSVGWhitespace(*aIter)) { + if (isCurrentCharEscaped) { + isCurrentCharEscaped = false; + } else { + if (*aIter == '+' || *aIter == '-' || + (aBreakOnDot && *aIter == '.')) { + break; + } + if (*aIter == '\\') { + isCurrentCharEscaped = true; + aIsAnyCharEscaped = true; + } + } + ++aIter; + } +} + +already_AddRefed +ConvertUnescapedTokenToAtom(const nsAString& aToken) +{ + // Whether the token is an id-ref or event-symbol it should be a valid NCName + if (aToken.IsEmpty() || NS_FAILED(nsContentUtils::CheckQName(aToken, false))) + return nullptr; + return NS_Atomize(aToken); +} + +already_AddRefed +ConvertTokenToAtom(const nsAString& aToken, + bool aUnescapeToken) +{ + // Unescaping involves making a copy of the string which we'd like to avoid if possible + if (!aUnescapeToken) { + return ConvertUnescapedTokenToAtom(aToken); + } + + nsAutoString token(aToken); + + const char16_t* read = token.BeginReading(); + const char16_t* const end = token.EndReading(); + char16_t* write = token.BeginWriting(); + bool escape = false; + + while (read != end) { + MOZ_ASSERT(write <= read, "Writing past where we've read"); + if (!escape && *read == '\\') { + escape = true; + ++read; + } else { + *write++ = *read++; + escape = false; + } + } + token.Truncate(write - token.BeginReading()); + + return ConvertUnescapedTokenToAtom(token); +} + +bool +ParseElementBaseTimeValueSpec(const nsAString& aSpec, + nsSMILTimeValueSpecParams& aResult) +{ + nsSMILTimeValueSpecParams result; + + // + // The spec will probably look something like one of these + // + // element-name.begin + // element-name.event-name + // event-name + // element-name.repeat(3) + // event\.name + // + // Technically `repeat(3)' is permitted but the behaviour in this case is not + // defined (for SMIL Animation) so we don't support it here. + // + + RangedPtr start(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + if (start == end) { + return false; + } + + RangedPtr tokenEnd(start); + + bool requiresUnescaping; + MoveToNextToken(tokenEnd, end, true, requiresUnescaping); + + RefPtr atom = + ConvertTokenToAtom(Substring(start.get(), tokenEnd.get()), + requiresUnescaping); + if (atom == nullptr) { + return false; + } + + // Parse the second token if there is one + if (tokenEnd != end && *tokenEnd == '.') { + result.mDependentElemID = atom; + + ++tokenEnd; + start = tokenEnd; + MoveToNextToken(tokenEnd, end, false, requiresUnescaping); + + const nsAString& token2 = Substring(start.get(), tokenEnd.get()); + + // element-name.begin + if (token2.EqualsLiteral("begin")) { + result.mType = nsSMILTimeValueSpecParams::SYNCBASE; + result.mSyncBegin = true; + // element-name.end + } else if (token2.EqualsLiteral("end")) { + result.mType = nsSMILTimeValueSpecParams::SYNCBASE; + result.mSyncBegin = false; + // element-name.repeat(digit+) + } else if (StringBeginsWith(token2, REPEAT_PREFIX)) { + start += REPEAT_PREFIX.Length(); + int32_t repeatValue; + if (start == tokenEnd || *start == '+' || *start == '-' || + !SVGContentUtils::ParseInteger(start, tokenEnd, repeatValue)) { + return false; + } + if (start == tokenEnd || *start != ')') { + return false; + } + result.mType = nsSMILTimeValueSpecParams::REPEAT; + result.mRepeatIterationOrAccessKey = repeatValue; + // element-name.event-symbol + } else { + atom = ConvertTokenToAtom(token2, requiresUnescaping); + if (atom == nullptr) { + return false; + } + result.mType = nsSMILTimeValueSpecParams::EVENT; + result.mEventSymbol = atom; + } + } else { + // event-symbol + result.mType = nsSMILTimeValueSpecParams::EVENT; + result.mEventSymbol = atom; + } + + // We've reached the end of the token, so we should now be either looking at + // a '+', '-' (possibly with whitespace before it), or the end. + if (!ParseOptionalOffset(tokenEnd, end, &result.mOffset) || tokenEnd != end) { + return false; + } + aResult = result; + return true; +} + +} // namespace + +//------------------------------------------------------------------------------ +// Implementation + +const nsDependentSubstring +nsSMILParserUtils::TrimWhitespace(const nsAString& aString) +{ + nsAString::const_iterator start, end; + + aString.BeginReading(start); + aString.EndReading(end); + + // Skip whitespace characters at the beginning + while (start != end && IsSVGWhitespace(*start)) { + ++start; + } + + // Skip whitespace characters at the end. + while (end != start) { + --end; + + if (!IsSVGWhitespace(*end)) { + // Step back to the last non-whitespace character. + ++end; + + break; + } + } + + return Substring(start, end); +} + +bool +nsSMILParserUtils::ParseKeySplines(const nsAString& aSpec, + FallibleTArray& aKeySplines) +{ + nsCharSeparatedTokenizerTemplate controlPointTokenizer(aSpec, ';'); + while (controlPointTokenizer.hasMoreTokens()) { + + nsCharSeparatedTokenizerTemplate + tokenizer(controlPointTokenizer.nextToken(), ',', + nsCharSeparatedTokenizer::SEPARATOR_OPTIONAL); + + double values[4]; + for (int i = 0 ; i < 4; i++) { + if (!tokenizer.hasMoreTokens() || + !SVGContentUtils::ParseNumber(tokenizer.nextToken(), values[i]) || + values[i] > 1.0 || values[i] < 0.0) { + return false; + } + } + if (tokenizer.hasMoreTokens() || + tokenizer.separatorAfterCurrentToken() || + !aKeySplines.AppendElement(nsSMILKeySpline(values[0], + values[1], + values[2], + values[3]), + fallible)) { + return false; + } + } + + return !aKeySplines.IsEmpty(); +} + +bool +nsSMILParserUtils::ParseSemicolonDelimitedProgressList(const nsAString& aSpec, + bool aNonDecreasing, + FallibleTArray& aArray) +{ + nsCharSeparatedTokenizerTemplate tokenizer(aSpec, ';'); + + double previousValue = -1.0; + + while (tokenizer.hasMoreTokens()) { + double value; + if (!SVGContentUtils::ParseNumber(tokenizer.nextToken(), value)) { + return false; + } + + if (value > 1.0 || value < 0.0 || + (aNonDecreasing && value < previousValue)) { + return false; + } + + if (!aArray.AppendElement(value, fallible)) { + return false; + } + previousValue = value; + } + + return !aArray.IsEmpty(); +} + +// Helper class for ParseValues +class MOZ_STACK_CLASS SMILValueParser : + public nsSMILParserUtils::GenericValueParser +{ +public: + SMILValueParser(const SVGAnimationElement* aSrcElement, + const nsISMILAttr* aSMILAttr, + FallibleTArray* aValuesArray, + bool* aPreventCachingOfSandwich) : + mSrcElement(aSrcElement), + mSMILAttr(aSMILAttr), + mValuesArray(aValuesArray), + mPreventCachingOfSandwich(aPreventCachingOfSandwich) + {} + + virtual bool Parse(const nsAString& aValueStr) override { + nsSMILValue newValue; + bool tmpPreventCachingOfSandwich = false; + if (NS_FAILED(mSMILAttr->ValueFromString(aValueStr, mSrcElement, newValue, + tmpPreventCachingOfSandwich))) + return false; + + if (!mValuesArray->AppendElement(newValue, fallible)) { + return false; + } + if (tmpPreventCachingOfSandwich) { + *mPreventCachingOfSandwich = true; + } + return true; + } +protected: + const SVGAnimationElement* mSrcElement; + const nsISMILAttr* mSMILAttr; + FallibleTArray* mValuesArray; + bool* mPreventCachingOfSandwich; +}; + +bool +nsSMILParserUtils::ParseValues(const nsAString& aSpec, + const SVGAnimationElement* aSrcElement, + const nsISMILAttr& aAttribute, + FallibleTArray& aValuesArray, + bool& aPreventCachingOfSandwich) +{ + // Assume all results can be cached, until we find one that can't. + aPreventCachingOfSandwich = false; + SMILValueParser valueParser(aSrcElement, &aAttribute, + &aValuesArray, &aPreventCachingOfSandwich); + return ParseValuesGeneric(aSpec, valueParser); +} + +bool +nsSMILParserUtils::ParseValuesGeneric(const nsAString& aSpec, + GenericValueParser& aParser) +{ + nsCharSeparatedTokenizerTemplate tokenizer(aSpec, ';'); + if (!tokenizer.hasMoreTokens()) { // Empty list + return false; + } + + while (tokenizer.hasMoreTokens()) { + if (!aParser.Parse(tokenizer.nextToken())) { + return false; + } + } + + return true; +} + +bool +nsSMILParserUtils::ParseRepeatCount(const nsAString& aSpec, + nsSMILRepeatCount& aResult) +{ + const nsAString& spec = + nsSMILParserUtils::TrimWhitespace(aSpec); + + if (spec.EqualsLiteral("indefinite")) { + aResult.SetIndefinite(); + return true; + } + + double value; + if (!SVGContentUtils::ParseNumber(spec, value) || value <= 0.0) { + return false; + } + aResult = value; + return true; +} + +bool +nsSMILParserUtils::ParseTimeValueSpecParams(const nsAString& aSpec, + nsSMILTimeValueSpecParams& aResult) +{ + const nsAString& spec = TrimWhitespace(aSpec); + + if (spec.EqualsLiteral("indefinite")) { + aResult.mType = nsSMILTimeValueSpecParams::INDEFINITE; + return true; + } + + // offset type + if (ParseOffsetValue(spec, &aResult.mOffset)) { + aResult.mType = nsSMILTimeValueSpecParams::OFFSET; + return true; + } + + // wallclock type + if (StringBeginsWith(spec, WALLCLOCK_PREFIX)) { + return false; // Wallclock times not implemented + } + + // accesskey type + if (StringBeginsWith(spec, ACCESSKEY_PREFIX_LC) || + StringBeginsWith(spec, ACCESSKEY_PREFIX_CC)) { + return ParseAccessKey(spec, aResult); + } + + // event, syncbase, or repeat + return ParseElementBaseTimeValueSpec(spec, aResult); +} + +bool +nsSMILParserUtils::ParseClockValue(const nsAString& aSpec, + nsSMILTimeValue* aResult) +{ + RangedPtr iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + return ::ParseClockValue(iter, end, aResult) && iter == end; +} + +int32_t +nsSMILParserUtils::CheckForNegativeNumber(const nsAString& aStr) +{ + int32_t absValLocation = -1; + + RangedPtr start(SVGContentUtils::GetStartRangedPtr(aStr)); + RangedPtr iter = start; + RangedPtr end(SVGContentUtils::GetEndRangedPtr(aStr)); + + // Skip initial whitespace + while (iter != end && IsSVGWhitespace(*iter)) { + ++iter; + } + + // Check for dash + if (iter != end && *iter == '-') { + ++iter; + // Check for numeric character + if (iter != end && SVGContentUtils::IsDigit(*iter)) { + absValLocation = iter - start; + } + } + return absValLocation; +} diff --git a/dom/smil/nsSMILParserUtils.h b/dom/smil/nsSMILParserUtils.h new file mode 100644 index 000000000..c80fd98a2 --- /dev/null +++ b/dom/smil/nsSMILParserUtils.h @@ -0,0 +1,89 @@ +/* -*- 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/. */ + +#ifndef NS_SMILPARSERUTILS_H_ +#define NS_SMILPARSERUTILS_H_ + +#include "nsTArray.h" +#include "nsStringFwd.h" + +class nsISMILAttr; +class nsSMILKeySpline; +class nsSMILTimeValue; +class nsSMILValue; +class nsSMILRepeatCount; +class nsSMILTimeValueSpecParams; + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +/** + * Common parsing utilities for the SMIL module. There is little re-use here; it + * simply serves to simplify other classes by moving parsing outside and to aid + * unit testing. + */ +class nsSMILParserUtils +{ +public: + // Abstract helper-class for assisting in parsing |values| attribute + class MOZ_STACK_CLASS GenericValueParser { + public: + virtual bool Parse(const nsAString& aValueStr) = 0; + }; + + static const nsDependentSubstring TrimWhitespace(const nsAString& aString); + + static bool ParseKeySplines(const nsAString& aSpec, + FallibleTArray& aKeySplines); + + // Used for parsing the |keyTimes| and |keyPoints| attributes. + static bool ParseSemicolonDelimitedProgressList(const nsAString& aSpec, + bool aNonDecreasing, + FallibleTArray& aArray); + + static bool ParseValues(const nsAString& aSpec, + const mozilla::dom::SVGAnimationElement* aSrcElement, + const nsISMILAttr& aAttribute, + FallibleTArray& aValuesArray, + bool& aPreventCachingOfSandwich); + + // Generic method that will run some code on each sub-section of an animation + // element's "values" list. + static bool ParseValuesGeneric(const nsAString& aSpec, + GenericValueParser& aParser); + + static bool ParseRepeatCount(const nsAString& aSpec, + nsSMILRepeatCount& aResult); + + static bool ParseTimeValueSpecParams(const nsAString& aSpec, + nsSMILTimeValueSpecParams& aResult); + + /* + * Parses a clock value as defined in the SMIL Animation specification. + * If parsing succeeds the returned value will be a non-negative, definite + * time value i.e. IsDefinite will return true. + * + * @param aSpec The string containing a clock value, e.g. "10s" + * @param aResult The parsed result. [OUT] + * @return true if parsing succeeded, otherwise false. + */ + static bool ParseClockValue(const nsAString& aSpec, + nsSMILTimeValue* aResult); + + /* + * This method checks whether the given string looks like a negative number. + * Specifically, it checks whether the string looks matches the pattern + * "[whitespace]*-[numeral].*" If the string matches this pattern, this + * method returns the index of the first character after the '-' sign + * (i.e. the index of the absolute value). If not, this method returns -1. + */ + static int32_t CheckForNegativeNumber(const nsAString& aStr); +}; + +#endif // NS_SMILPARSERUTILS_H_ diff --git a/dom/smil/nsSMILRepeatCount.cpp b/dom/smil/nsSMILRepeatCount.cpp new file mode 100644 index 000000000..d5c61fe53 --- /dev/null +++ b/dom/smil/nsSMILRepeatCount.cpp @@ -0,0 +1,10 @@ +/* -*- 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 "nsSMILRepeatCount.h" + +/*static*/ const double nsSMILRepeatCount::kNotSet = -1.0; +/*static*/ const double nsSMILRepeatCount::kIndefinite = -2.0; diff --git a/dom/smil/nsSMILRepeatCount.h b/dom/smil/nsSMILRepeatCount.h new file mode 100644 index 000000000..be36badee --- /dev/null +++ b/dom/smil/nsSMILRepeatCount.h @@ -0,0 +1,62 @@ +/* -*- 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/. */ + +#ifndef nsSMILRepeatCount_h +#define nsSMILRepeatCount_h + +#include "nsDebug.h" +#include + +//---------------------------------------------------------------------- +// nsSMILRepeatCount +// +// A tri-state non-negative floating point number for representing the number of +// times an animation repeat, i.e. the SMIL repeatCount attribute. +// +// The three states are: +// 1. not-set +// 2. set (with non-negative, non-zero count value) +// 3. indefinite +// +class nsSMILRepeatCount +{ +public: + nsSMILRepeatCount() : mCount(kNotSet) {} + explicit nsSMILRepeatCount(double aCount) + : mCount(kNotSet) { SetCount(aCount); } + + operator double() const { + MOZ_ASSERT(IsDefinite(), + "Converting indefinite or unset repeat count to double"); + return mCount; + } + bool IsDefinite() const { + return mCount != kNotSet && mCount != kIndefinite; + } + bool IsIndefinite() const { return mCount == kIndefinite; } + bool IsSet() const { return mCount != kNotSet; } + + nsSMILRepeatCount& operator=(double aCount) + { + SetCount(aCount); + return *this; + } + void SetCount(double aCount) + { + NS_ASSERTION(aCount > 0.0, "Negative or zero repeat count"); + mCount = aCount > 0.0 ? aCount : kNotSet; + } + void SetIndefinite() { mCount = kIndefinite; } + void Unset() { mCount = kNotSet; } + +private: + static const double kNotSet; + static const double kIndefinite; + + double mCount; +}; + +#endif diff --git a/dom/smil/nsSMILSetAnimationFunction.cpp b/dom/smil/nsSMILSetAnimationFunction.cpp new file mode 100644 index 000000000..d636f9aff --- /dev/null +++ b/dom/smil/nsSMILSetAnimationFunction.cpp @@ -0,0 +1,101 @@ +/* -*- 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 "nsSMILSetAnimationFunction.h" + +inline bool +nsSMILSetAnimationFunction::IsDisallowedAttribute( + const nsIAtom* aAttribute) const +{ + // + // A element is similar to but lacks: + // AnimationValue.attrib(calcMode, values, keyTimes, keySplines, from, to, + // by) -- BUT has 'to' + // AnimationAddition.attrib(additive, accumulate) + // + if (aAttribute == nsGkAtoms::calcMode || + aAttribute == nsGkAtoms::values || + aAttribute == nsGkAtoms::keyTimes || + aAttribute == nsGkAtoms::keySplines || + aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::by || + aAttribute == nsGkAtoms::additive || + aAttribute == nsGkAtoms::accumulate) { + return true; + } + + return false; +} + +bool +nsSMILSetAnimationFunction::SetAttr(nsIAtom* aAttribute, + const nsAString& aValue, + nsAttrValue& aResult, + nsresult* aParseResult) +{ + if (IsDisallowedAttribute(aAttribute)) { + aResult.SetTo(aValue); + if (aParseResult) { + // SMILANIM 4.2 says: + // + // The additive and accumulate attributes are not allowed, and will be + // ignored if specified. + // + // So at least for those two attributes we shouldn't report an error even + // if they're present. For now we'll also just silently ignore other + // attribute types too. + *aParseResult = NS_OK; + } + return true; + } + + return nsSMILAnimationFunction::SetAttr(aAttribute, aValue, + aResult, aParseResult); +} + +bool +nsSMILSetAnimationFunction::UnsetAttr(nsIAtom* aAttribute) +{ + if (IsDisallowedAttribute(aAttribute)) { + return true; + } + + return nsSMILAnimationFunction::UnsetAttr(aAttribute); +} + +bool +nsSMILSetAnimationFunction::HasAttr(nsIAtom* aAttName) const +{ + if (IsDisallowedAttribute(aAttName)) + return false; + + return nsSMILAnimationFunction::HasAttr(aAttName); +} + +const nsAttrValue* +nsSMILSetAnimationFunction::GetAttr(nsIAtom* aAttName) const +{ + if (IsDisallowedAttribute(aAttName)) + return nullptr; + + return nsSMILAnimationFunction::GetAttr(aAttName); +} + +bool +nsSMILSetAnimationFunction::GetAttr(nsIAtom* aAttName, + nsAString& aResult) const +{ + if (IsDisallowedAttribute(aAttName)) + return false; + + return nsSMILAnimationFunction::GetAttr(aAttName, aResult); +} + +bool +nsSMILSetAnimationFunction::WillReplace() const +{ + return true; +} diff --git a/dom/smil/nsSMILSetAnimationFunction.h b/dom/smil/nsSMILSetAnimationFunction.h new file mode 100644 index 000000000..6401a3bf5 --- /dev/null +++ b/dom/smil/nsSMILSetAnimationFunction.h @@ -0,0 +1,68 @@ +/* -*- 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/. */ + +#ifndef NS_SMILSETANIMATIONFUNCTION_H_ +#define NS_SMILSETANIMATIONFUNCTION_H_ + +#include "mozilla/Attributes.h" +#include "nsSMILAnimationFunction.h" + +//---------------------------------------------------------------------- +// nsSMILSetAnimationFunction +// +// Subclass of nsSMILAnimationFunction that limits the behaviour to that offered +// by a element. +// +class nsSMILSetAnimationFunction : public nsSMILAnimationFunction +{ +public: + /* + * Sets animation-specific attributes (or marks them dirty, in the case + * of from/to/by/values). + * + * @param aAttribute The attribute being set + * @param aValue The updated value of the attribute. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aParseResult Outparam used for reporting parse errors. Will be set + * to NS_OK if everything succeeds. + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, nsresult* aParseResult = nullptr) override; + + /* + * Unsets the given attribute. + * + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool UnsetAttr(nsIAtom* aAttribute) override; + +protected: + // Although animation might look like to-animation, unlike to-animation, + // it never interpolates values. + // Returning false here will mean this animation function gets treated as + // a single-valued function and no interpolation will be attempted. + virtual bool IsToAnimation() const override { + return false; + } + + // applies the exact same value across the simple duration. + virtual bool IsValueFixedForSimpleDuration() const override { + return true; + } + virtual bool HasAttr(nsIAtom* aAttName) const override; + virtual const nsAttrValue* GetAttr(nsIAtom* aAttName) const override; + virtual bool GetAttr(nsIAtom* aAttName, + nsAString& aResult) const override; + virtual bool WillReplace() const override; + + bool IsDisallowedAttribute(const nsIAtom* aAttribute) const; +}; + +#endif // NS_SMILSETANIMATIONFUNCTION_H_ diff --git a/dom/smil/nsSMILTargetIdentifier.h b/dom/smil/nsSMILTargetIdentifier.h new file mode 100644 index 000000000..56fb08f34 --- /dev/null +++ b/dom/smil/nsSMILTargetIdentifier.h @@ -0,0 +1,86 @@ +/* -*- 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/. */ + +#ifndef NS_SMILTARGETIDENTIFIER_H_ +#define NS_SMILTARGETIDENTIFIER_H_ + +#include "mozilla/dom/Element.h" + +/** + * Struct: nsSMILTargetIdentifier + * + * Tuple of: { Animated Element, Attribute Name, Attribute Type (CSS vs. XML) } + * + * Used in nsSMILAnimationController as hash key for mapping an animation + * target to the nsSMILCompositor for that target. + * + * NOTE: Need a nsRefPtr for the element & attribute name, because + * nsSMILAnimationController retain its hash table for one sample into the + * future, and we need to make sure their target isn't deleted in that time. + */ + +struct nsSMILTargetIdentifier +{ + nsSMILTargetIdentifier() + : mElement(nullptr), mAttributeName(nullptr), + mAttributeNamespaceID(kNameSpaceID_Unknown), mIsCSS(false) {} + + inline bool Equals(const nsSMILTargetIdentifier& aOther) const + { + return (aOther.mElement == mElement && + aOther.mAttributeName == mAttributeName && + aOther.mAttributeNamespaceID == mAttributeNamespaceID && + aOther.mIsCSS == mIsCSS); + } + + RefPtr mElement; + RefPtr mAttributeName; + int32_t mAttributeNamespaceID; + bool mIsCSS; +}; + +/** + * Class: nsSMILWeakTargetIdentifier + * + * Version of the above struct that uses non-owning pointers. These are kept + * private, to ensure that they aren't ever dereferenced (or used at all, + * outside of Equals()). + * + * This is solely for comparisons to determine if a target has changed + * from one sample to the next. + */ +class nsSMILWeakTargetIdentifier +{ +public: + // Trivial constructor + nsSMILWeakTargetIdentifier() + : mElement(nullptr), mAttributeName(nullptr), mIsCSS(false) {} + + // Allow us to update a weak identifier to match a given non-weak identifier + nsSMILWeakTargetIdentifier& + operator=(const nsSMILTargetIdentifier& aOther) + { + mElement = aOther.mElement; + mAttributeName = aOther.mAttributeName; + mIsCSS = aOther.mIsCSS; + return *this; + } + + // Allow for comparison vs. non-weak identifier + inline bool Equals(const nsSMILTargetIdentifier& aOther) const + { + return (aOther.mElement == mElement && + aOther.mAttributeName == mAttributeName && + aOther.mIsCSS == mIsCSS); + } + +private: + const nsIContent* mElement; + const nsIAtom* mAttributeName; + bool mIsCSS; +}; + +#endif // NS_SMILTARGETIDENTIFIER_H_ diff --git a/dom/smil/nsSMILTimeContainer.cpp b/dom/smil/nsSMILTimeContainer.cpp new file mode 100644 index 000000000..3df8a64ca --- /dev/null +++ b/dom/smil/nsSMILTimeContainer.cpp @@ -0,0 +1,339 @@ +/* -*- 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 "nsSMILTimeContainer.h" +#include "nsSMILTimeValue.h" +#include "nsSMILTimedElement.h" +#include + +#include "mozilla/AutoRestore.h" + +nsSMILTimeContainer::nsSMILTimeContainer() +: + mParent(nullptr), + mCurrentTime(0L), + mParentOffset(0L), + mPauseStart(0L), + mNeedsPauseSample(false), + mNeedsRewind(false), + mIsSeeking(false), + mHoldingEntries(false), + mPauseState(PAUSE_BEGIN) +{ +} + +nsSMILTimeContainer::~nsSMILTimeContainer() +{ + if (mParent) { + mParent->RemoveChild(*this); + } +} + +nsSMILTimeValue +nsSMILTimeContainer::ContainerToParentTime(nsSMILTime aContainerTime) const +{ + // If we're paused, then future times are indefinite + if (IsPaused() && aContainerTime > mCurrentTime) + return nsSMILTimeValue::Indefinite(); + + return nsSMILTimeValue(aContainerTime + mParentOffset); +} + +nsSMILTimeValue +nsSMILTimeContainer::ParentToContainerTime(nsSMILTime aParentTime) const +{ + // If we're paused, then any time after when we paused is indefinite + if (IsPaused() && aParentTime > mPauseStart) + return nsSMILTimeValue::Indefinite(); + + return nsSMILTimeValue(aParentTime - mParentOffset); +} + +void +nsSMILTimeContainer::Begin() +{ + Resume(PAUSE_BEGIN); + if (mPauseState) { + mNeedsPauseSample = true; + } + + // This is a little bit complicated here. Ideally we'd just like to call + // Sample() and force an initial sample but this turns out to be a bad idea + // because this may mean that NeedsSample() no longer reports true and so when + // we come to the first real sample our parent will skip us over altogether. + // So we force the time to be updated and adopt the policy to never call + // Sample() ourselves but to always leave that to our parent or client. + + UpdateCurrentTime(); +} + +void +nsSMILTimeContainer::Pause(uint32_t aType) +{ + bool didStartPause = false; + + if (!mPauseState && aType) { + mPauseStart = GetParentTime(); + mNeedsPauseSample = true; + didStartPause = true; + } + + mPauseState |= aType; + + if (didStartPause) { + NotifyTimeChange(); + } +} + +void +nsSMILTimeContainer::Resume(uint32_t aType) +{ + if (!mPauseState) + return; + + mPauseState &= ~aType; + + if (!mPauseState) { + nsSMILTime extraOffset = GetParentTime() - mPauseStart; + mParentOffset += extraOffset; + NotifyTimeChange(); + } +} + +nsSMILTime +nsSMILTimeContainer::GetCurrentTime() const +{ + // The following behaviour is consistent with: + // http://www.w3.org/2003/01/REC-SVG11-20030114-errata + // #getCurrentTime_setCurrentTime_undefined_before_document_timeline_begin + // which says that if GetCurrentTime is called before the document timeline + // has begun we should just return 0. + if (IsPausedByType(PAUSE_BEGIN)) + return 0L; + + return mCurrentTime; +} + +void +nsSMILTimeContainer::SetCurrentTime(nsSMILTime aSeekTo) +{ + // SVG 1.1 doesn't specify what to do for negative times so we adopt SVGT1.2's + // behaviour of clamping negative times to 0. + aSeekTo = std::max(0, aSeekTo); + + // The following behaviour is consistent with: + // http://www.w3.org/2003/01/REC-SVG11-20030114-errata + // #getCurrentTime_setCurrentTime_undefined_before_document_timeline_begin + // which says that if SetCurrentTime is called before the document timeline + // has begun we should still adjust the offset. + nsSMILTime parentTime = GetParentTime(); + mParentOffset = parentTime - aSeekTo; + mIsSeeking = true; + + if (IsPaused()) { + mNeedsPauseSample = true; + mPauseStart = parentTime; + } + + if (aSeekTo < mCurrentTime) { + // Backwards seek + mNeedsRewind = true; + ClearMilestones(); + } + + // Force an update to the current time in case we get a call to GetCurrentTime + // before another call to Sample(). + UpdateCurrentTime(); + + NotifyTimeChange(); +} + +nsSMILTime +nsSMILTimeContainer::GetParentTime() const +{ + if (mParent) + return mParent->GetCurrentTime(); + + return 0L; +} + +void +nsSMILTimeContainer::SyncPauseTime() +{ + if (IsPaused()) { + nsSMILTime parentTime = GetParentTime(); + nsSMILTime extraOffset = parentTime - mPauseStart; + mParentOffset += extraOffset; + mPauseStart = parentTime; + } +} + +void +nsSMILTimeContainer::Sample() +{ + if (!NeedsSample()) + return; + + UpdateCurrentTime(); + DoSample(); + + mNeedsPauseSample = false; +} + +nsresult +nsSMILTimeContainer::SetParent(nsSMILTimeContainer* aParent) +{ + if (mParent) { + mParent->RemoveChild(*this); + // When we're not attached to a parent time container, GetParentTime() will + // return 0. We need to adjust our pause state information to be relative to + // this new time base. + // Note that since "current time = parent time - parent offset" setting the + // parent offset and pause start as follows preserves our current time even + // while parent time = 0. + mParentOffset = -mCurrentTime; + mPauseStart = 0L; + } + + mParent = aParent; + + nsresult rv = NS_OK; + if (mParent) { + rv = mParent->AddChild(*this); + } + + return rv; +} + +bool +nsSMILTimeContainer::AddMilestone(const nsSMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement) +{ + // We record the milestone time and store it along with the element but this + // time may change (e.g. if attributes are changed on the timed element in + // between samples). If this happens, then we may do an unecessary sample + // but that's pretty cheap. + MOZ_RELEASE_ASSERT(!mHoldingEntries); + return mMilestoneEntries.Push(MilestoneEntry(aMilestone, aElement)); +} + +void +nsSMILTimeContainer::ClearMilestones() +{ + MOZ_RELEASE_ASSERT(!mHoldingEntries); + mMilestoneEntries.Clear(); +} + +bool +nsSMILTimeContainer::GetNextMilestoneInParentTime( + nsSMILMilestone& aNextMilestone) const +{ + if (mMilestoneEntries.IsEmpty()) + return false; + + nsSMILTimeValue parentTime = + ContainerToParentTime(mMilestoneEntries.Top().mMilestone.mTime); + if (!parentTime.IsDefinite()) + return false; + + aNextMilestone = nsSMILMilestone(parentTime.GetMillis(), + mMilestoneEntries.Top().mMilestone.mIsEnd); + + return true; +} + +bool +nsSMILTimeContainer::PopMilestoneElementsAtMilestone( + const nsSMILMilestone& aMilestone, + AnimElemArray& aMatchedElements) +{ + if (mMilestoneEntries.IsEmpty()) + return false; + + nsSMILTimeValue containerTime = ParentToContainerTime(aMilestone.mTime); + if (!containerTime.IsDefinite()) + return false; + + nsSMILMilestone containerMilestone(containerTime.GetMillis(), + aMilestone.mIsEnd); + + MOZ_ASSERT(mMilestoneEntries.Top().mMilestone >= containerMilestone, + "Trying to pop off earliest times but we have earlier ones that " + "were overlooked"); + + MOZ_RELEASE_ASSERT(!mHoldingEntries); + + bool gotOne = false; + while (!mMilestoneEntries.IsEmpty() && + mMilestoneEntries.Top().mMilestone == containerMilestone) + { + aMatchedElements.AppendElement(mMilestoneEntries.Pop().mTimebase); + gotOne = true; + } + + return gotOne; +} + +void +nsSMILTimeContainer::Traverse(nsCycleCollectionTraversalCallback* aCallback) +{ + AutoRestore saveHolding(mHoldingEntries); + mHoldingEntries = true; + const MilestoneEntry* p = mMilestoneEntries.Elements(); + while (p < mMilestoneEntries.Elements() + mMilestoneEntries.Length()) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCallback, "mTimebase"); + aCallback->NoteXPCOMChild(static_cast(p->mTimebase.get())); + ++p; + } +} + +void +nsSMILTimeContainer::Unlink() +{ + MOZ_RELEASE_ASSERT(!mHoldingEntries); + mMilestoneEntries.Clear(); +} + +void +nsSMILTimeContainer::UpdateCurrentTime() +{ + nsSMILTime now = IsPaused() ? mPauseStart : GetParentTime(); + mCurrentTime = now - mParentOffset; + MOZ_ASSERT(mCurrentTime >= 0, "Container has negative time"); +} + +void +nsSMILTimeContainer::NotifyTimeChange() +{ + // Called when the container time is changed with respect to the document + // time. When this happens time dependencies in other time containers need to + // re-resolve their times because begin and end times are stored in container + // time. + // + // To get the list of timed elements with dependencies we simply re-use the + // milestone elements. This is because any timed element with dependents and + // with significant transitions yet to fire should have their next milestone + // registered. Other timed elements don't matter. + + // Copy the timed elements to a separate array before calling + // HandleContainerTimeChange on each of them in case doing so mutates + // mMilestoneEntries. + nsTArray> elems; + + { + AutoRestore saveHolding(mHoldingEntries); + mHoldingEntries = true; + for (const MilestoneEntry* p = mMilestoneEntries.Elements(); + p < mMilestoneEntries.Elements() + mMilestoneEntries.Length(); + ++p) { + elems.AppendElement(p->mTimebase.get()); + } + } + + for (auto& elem : elems) { + elem->TimedElement().HandleContainerTimeChange(); + } +} diff --git a/dom/smil/nsSMILTimeContainer.h b/dom/smil/nsSMILTimeContainer.h new file mode 100644 index 000000000..50c9709db --- /dev/null +++ b/dom/smil/nsSMILTimeContainer.h @@ -0,0 +1,298 @@ +/* -*- 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/. */ + +#ifndef NS_SMILTIMECONTAINER_H_ +#define NS_SMILTIMECONTAINER_H_ + +#include "mozilla/dom/SVGAnimationElement.h" +#include "nscore.h" +#include "nsSMILTypes.h" +#include "nsTPriorityQueue.h" +#include "nsSMILMilestone.h" + +class nsSMILTimeValue; + +//---------------------------------------------------------------------- +// nsSMILTimeContainer +// +// Common base class for a time base that can be paused, resumed, and sampled. +// +class nsSMILTimeContainer +{ +public: + nsSMILTimeContainer(); + virtual ~nsSMILTimeContainer(); + + /* + * Pause request types. + */ + enum { + PAUSE_BEGIN = 1, // Paused because timeline has yet to begin. + PAUSE_SCRIPT = 2, // Paused by script. + PAUSE_PAGEHIDE = 4, // Paused because our doc is hidden. + PAUSE_USERPREF = 8, // Paused because animations are disabled in prefs. + PAUSE_IMAGE = 16 // Paused becuase we're in an image that's suspended. + }; + + /* + * Cause the time container to record its begin time. + */ + void Begin(); + + /* + * Pause this time container + * + * @param aType The source of the pause request. Successive calls to Pause + * with the same aType will be ignored. The container will remain paused until + * each call to Pause of a given aType has been matched by at least one call + * to Resume with the same aType. + */ + virtual void Pause(uint32_t aType); + + /* + * Resume this time container + * + * param @aType The source of the resume request. Clears the pause flag for + * this particular type of pause request. When all pause flags have been + * cleared the time container will be resumed. + */ + virtual void Resume(uint32_t aType); + + /** + * Returns true if this time container is paused by the specified type. + * Note that the time container may also be paused by other types; this method + * does not test if aType is the exclusive pause source. + * + * @param @aType The pause source to test for. + * @return true if this container is paused by aType. + */ + bool IsPausedByType(uint32_t aType) const { return mPauseState & aType; } + + /** + * Returns true if this time container is paused. + * Generally you should test for a specific type of pausing using + * IsPausedByType. + * + * @return true if this container is paused, false otherwise. + */ + bool IsPaused() const { return mPauseState != 0; } + + /* + * Return the time elapsed since this time container's begin time (expressed + * in parent time) minus any accumulated offset from pausing. + */ + nsSMILTime GetCurrentTime() const; + + /* + * Seek the document timeline to the specified time. + * + * @param aSeekTo The time to seek to, expressed in this time container's time + * base (i.e. the same units as GetCurrentTime). + */ + void SetCurrentTime(nsSMILTime aSeekTo); + + /* + * Return the current time for the parent time container if any. + */ + virtual nsSMILTime GetParentTime() const; + + /* + * Convert container time to parent time. + * + * @param aContainerTime The container time to convert. + * @return The equivalent parent time or indefinite if the container is + * paused and the time is in the future. + */ + nsSMILTimeValue ContainerToParentTime(nsSMILTime aContainerTime) const; + + /* + * Convert from parent time to container time. + * + * @param aParentTime The parent time to convert. + * @return The equivalent container time or indefinite if the container is + * paused and aParentTime is after the time when the pause began. + */ + nsSMILTimeValue ParentToContainerTime(nsSMILTime aParentTime) const; + + /* + * If the container is paused, causes the pause time to be updated to the + * current parent time. This should be called before updating + * cross-container dependencies that will call ContainerToParentTime in order + * to provide more intuitive results. + */ + void SyncPauseTime(); + + /* + * Updates the current time of this time container and calls DoSample to + * perform any sample-operations. + */ + void Sample(); + + /* + * Return if this time container should be sampled or can be skipped. + * + * This is most useful as an optimisation for skipping time containers that + * don't require a sample. + */ + bool NeedsSample() const { return !mPauseState || mNeedsPauseSample; } + + /* + * Indicates if the elements of this time container need to be rewound. + * This occurs during a backwards seek. + */ + bool NeedsRewind() const { return mNeedsRewind; } + void ClearNeedsRewind() { mNeedsRewind = false; } + + /* + * Indicates the time container is currently processing a SetCurrentTime + * request and appropriate seek behaviour should be applied by child elements + * (e.g. not firing time events). + */ + bool IsSeeking() const { return mIsSeeking; } + void MarkSeekFinished() { mIsSeeking = false; } + + /* + * Sets the parent time container. + * + * The callee still retains ownership of the time container. + */ + nsresult SetParent(nsSMILTimeContainer* aParent); + + /* + * Registers an element for a sample at the given time. + * + * @param aMilestone The milestone to register in container time. + * @param aElement The timebase element that needs a sample at + * aMilestone. + * @return true if the element was successfully added, false otherwise. + */ + bool AddMilestone(const nsSMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement); + + /* + * Resets the list of milestones. + */ + void ClearMilestones(); + + /* + * Returns the next significant transition from amongst the registered + * milestones. + * + * @param[out] aNextMilestone The next milestone with time in parent time. + * + * @return true if there exists another milestone, false otherwise in + * which case aNextMilestone will be unmodified. + */ + bool GetNextMilestoneInParentTime(nsSMILMilestone& aNextMilestone) const; + + typedef nsTArray > AnimElemArray; + + /* + * Removes and returns the timebase elements from the start of the list of + * timebase elements that match the given time. + * + * @param aMilestone The milestone time to match in parent time. This + * must be <= GetNextMilestoneInParentTime. + * @param[out] aMatchedElements The array to which matching elements will be + * appended. + * @return true if one or more elements match, false otherwise. + */ + bool PopMilestoneElementsAtMilestone(const nsSMILMilestone& aMilestone, + AnimElemArray& aMatchedElements); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + +protected: + /* + * Per-sample operations to be performed whenever Sample() is called and + * NeedsSample() is true. Called after updating mCurrentTime; + */ + virtual void DoSample() { } + + /* + * Adding and removing child containers is not implemented in the base class + * because not all subclasses need this. + */ + + /* + * Adds a child time container. + */ + virtual nsresult AddChild(nsSMILTimeContainer& aChild) + { + return NS_ERROR_FAILURE; + } + + /* + * Removes a child time container. + */ + virtual void RemoveChild(nsSMILTimeContainer& aChild) { } + + /* + * Implementation helper to update the current time. + */ + void UpdateCurrentTime(); + + /* + * Implementation helper to notify timed elements with dependencies that the + * container time has changed with respect to the document time. + */ + void NotifyTimeChange(); + + // The parent time container, if any + nsSMILTimeContainer* mParent; + + // The current time established at the last call to Sample() + nsSMILTime mCurrentTime; + + // The number of milliseconds for which the container has been paused + // (excluding the current pause interval if the container is currently + // paused). + // + // Current time = parent time - mParentOffset + // + nsSMILTime mParentOffset; + + // The timestamp in parent time when the container was paused + nsSMILTime mPauseStart; + + // Whether or not a pause sample is required + bool mNeedsPauseSample; + + bool mNeedsRewind; // Backwards seek performed + bool mIsSeeking; // Currently in the middle of a seek operation + + bool mHoldingEntries; // True if there's a raw pointer to mMilestoneEntries on the stack. + + // A bitfield of the pause state for all pause requests + uint32_t mPauseState; + + struct MilestoneEntry + { + MilestoneEntry(nsSMILMilestone aMilestone, + mozilla::dom::SVGAnimationElement& aElement) + : mMilestone(aMilestone), mTimebase(&aElement) + { } + + bool operator<(const MilestoneEntry& aOther) const + { + return mMilestone < aOther.mMilestone; + } + + nsSMILMilestone mMilestone; // In container time. + RefPtr mTimebase; + }; + + // Queue of elements with registered milestones. Used to update the model with + // significant transitions that occur between two samples. Since timed element + // re-register their milestones when they're sampled this is reset once we've + // taken care of the milestones before the current sample time but before we + // actually do the full sample. + nsTPriorityQueue mMilestoneEntries; +}; + +#endif // NS_SMILTIMECONTAINER_H_ diff --git a/dom/smil/nsSMILTimeValue.cpp b/dom/smil/nsSMILTimeValue.cpp new file mode 100644 index 000000000..ddd5b3ce7 --- /dev/null +++ b/dom/smil/nsSMILTimeValue.cpp @@ -0,0 +1,41 @@ +/* -*- 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 "nsSMILTimeValue.h" + +nsSMILTime nsSMILTimeValue::kUnresolvedMillis = INT64_MAX; + +//---------------------------------------------------------------------- +// nsSMILTimeValue methods: + +static inline int8_t +Cmp(int64_t aA, int64_t aB) +{ + return aA == aB ? 0 : (aA > aB ? 1 : -1); +} + +int8_t +nsSMILTimeValue::CompareTo(const nsSMILTimeValue& aOther) const +{ + int8_t result; + + if (mState == STATE_DEFINITE) { + result = (aOther.mState == STATE_DEFINITE) + ? Cmp(mMilliseconds, aOther.mMilliseconds) + : -1; + } else if (mState == STATE_INDEFINITE) { + if (aOther.mState == STATE_DEFINITE) + result = 1; + else if (aOther.mState == STATE_INDEFINITE) + result = 0; + else + result = -1; + } else { + result = (aOther.mState != STATE_UNRESOLVED) ? 1 : 0; + } + + return result; +} diff --git a/dom/smil/nsSMILTimeValue.h b/dom/smil/nsSMILTimeValue.h new file mode 100644 index 000000000..de694bdbf --- /dev/null +++ b/dom/smil/nsSMILTimeValue.h @@ -0,0 +1,135 @@ +/* -*- 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/. */ + +#ifndef NS_SMILTIMEVALUE_H_ +#define NS_SMILTIMEVALUE_H_ + +#include "nsSMILTypes.h" +#include "nsDebug.h" + +/*---------------------------------------------------------------------- + * nsSMILTimeValue class + * + * A tri-state time value. + * + * First a quick overview of the SMIL time data types: + * + * nsSMILTime -- a timestamp in milliseconds. + * nsSMILTimeValue -- (this class) a timestamp that can take the additional + * states 'indefinite' and 'unresolved' + * nsSMILInstanceTime -- an nsSMILTimeValue used for constructing intervals. It + * contains additional fields to govern reset behavior + * and track timing dependencies (e.g. syncbase timing). + * nsSMILInterval -- a pair of nsSMILInstanceTimes that defines a begin and + * an end time for animation. + * nsSMILTimeValueSpec -- a component of a begin or end attribute, such as the + * '5s' or 'a.end+2m' in begin="5s; a.end+2m". Acts as + * a broker between an nsSMILTimedElement and its + * nsSMILInstanceTimes by generating new instance times + * and handling changes to existing times. + * + * Objects of this class may be in one of three states: + * + * 1) The time is resolved and has a definite millisecond value + * 2) The time is resolved and indefinite + * 3) The time is unresolved + * + * In summary: + * + * State | GetMillis | IsDefinite | IsIndefinite | IsResolved + * -----------+-----------------+------------+--------------+------------ + * Definite | nsSMILTimeValue | true | false | true + * -----------+-----------------+------------+--------------+------------ + * Indefinite | -- | false | true | true + * -----------+-----------------+------------+--------------+------------ + * Unresolved | -- | false | false | false + * + */ + +class nsSMILTimeValue +{ +public: + // Creates an unresolved time value + nsSMILTimeValue() + : mMilliseconds(kUnresolvedMillis), + mState(STATE_UNRESOLVED) + { } + + // Creates a resolved time value + explicit nsSMILTimeValue(nsSMILTime aMillis) + : mMilliseconds(aMillis), + mState(STATE_DEFINITE) + { } + + // Named constructor to create an indefinite time value + static nsSMILTimeValue Indefinite() + { + nsSMILTimeValue value; + value.SetIndefinite(); + return value; + } + + bool IsIndefinite() const { return mState == STATE_INDEFINITE; } + void SetIndefinite() + { + mState = STATE_INDEFINITE; + mMilliseconds = kUnresolvedMillis; + } + + bool IsResolved() const { return mState != STATE_UNRESOLVED; } + void SetUnresolved() + { + mState = STATE_UNRESOLVED; + mMilliseconds = kUnresolvedMillis; + } + + bool IsDefinite() const { return mState == STATE_DEFINITE; } + nsSMILTime GetMillis() const + { + MOZ_ASSERT(mState == STATE_DEFINITE, + "GetMillis() called for unresolved or indefinite time"); + + return mState == STATE_DEFINITE ? mMilliseconds : kUnresolvedMillis; + } + + void SetMillis(nsSMILTime aMillis) + { + mState = STATE_DEFINITE; + mMilliseconds = aMillis; + } + + int8_t CompareTo(const nsSMILTimeValue& aOther) const; + + bool operator==(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) == 0; } + + bool operator!=(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) != 0; } + + bool operator<(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) < 0; } + + bool operator>(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) > 0; } + + bool operator<=(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) <= 0; } + + bool operator>=(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) >= 0; } + +private: + static nsSMILTime kUnresolvedMillis; + + nsSMILTime mMilliseconds; + enum { + STATE_DEFINITE, + STATE_INDEFINITE, + STATE_UNRESOLVED + } mState; +}; + +#endif // NS_SMILTIMEVALUE_H_ diff --git a/dom/smil/nsSMILTimeValueSpec.cpp b/dom/smil/nsSMILTimeValueSpec.cpp new file mode 100644 index 000000000..6948b5d58 --- /dev/null +++ b/dom/smil/nsSMILTimeValueSpec.cpp @@ -0,0 +1,536 @@ +/* -*- 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 "mozilla/EventListenerManager.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsSMILTimeValueSpec.h" +#include "nsSMILInterval.h" +#include "nsSMILTimeContainer.h" +#include "nsSMILTimeValue.h" +#include "nsSMILTimedElement.h" +#include "nsSMILInstanceTime.h" +#include "nsSMILParserUtils.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMTimeEvent.h" +#include "nsString.h" +#include + +using namespace mozilla; +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// Nested class: EventListener + +NS_IMPL_ISUPPORTS(nsSMILTimeValueSpec::EventListener, nsIDOMEventListener) + +NS_IMETHODIMP +nsSMILTimeValueSpec::EventListener::HandleEvent(nsIDOMEvent* aEvent) +{ + if (mSpec) { + mSpec->HandleEvent(aEvent); + } + return NS_OK; +} + +//---------------------------------------------------------------------- +// Implementation + +nsSMILTimeValueSpec::nsSMILTimeValueSpec(nsSMILTimedElement& aOwner, + bool aIsBegin) + : mOwner(&aOwner), + mIsBegin(aIsBegin), + mReferencedElement(this) +{ +} + +nsSMILTimeValueSpec::~nsSMILTimeValueSpec() +{ + UnregisterFromReferencedElement(mReferencedElement.get()); + if (mEventListener) { + mEventListener->Disconnect(); + mEventListener = nullptr; + } +} + +nsresult +nsSMILTimeValueSpec::SetSpec(const nsAString& aStringSpec, + Element* aContextNode) +{ + nsSMILTimeValueSpecParams params; + + if (!nsSMILParserUtils::ParseTimeValueSpecParams(aStringSpec, params)) + return NS_ERROR_FAILURE; + + mParams = params; + + // According to SMIL 3.0: + // The special value "indefinite" does not yield an instance time in the + // begin list. It will, however yield a single instance with the value + // "indefinite" in an end list. This value is not removed by a reset. + if (mParams.mType == nsSMILTimeValueSpecParams::OFFSET || + (!mIsBegin && mParams.mType == nsSMILTimeValueSpecParams::INDEFINITE)) { + mOwner->AddInstanceTime(new nsSMILInstanceTime(mParams.mOffset), mIsBegin); + } + + // Fill in the event symbol to simplify handling later + if (mParams.mType == nsSMILTimeValueSpecParams::REPEAT) { + mParams.mEventSymbol = nsGkAtoms::repeatEvent; + } else if (mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY) { + mParams.mEventSymbol = nsGkAtoms::keypress; + } + + ResolveReferences(aContextNode); + + return NS_OK; +} + +void +nsSMILTimeValueSpec::ResolveReferences(nsIContent* aContextNode) +{ + if (mParams.mType != nsSMILTimeValueSpecParams::SYNCBASE && !IsEventBased()) + return; + + MOZ_ASSERT(aContextNode, + "null context node for resolving timing references against"); + + // If we're not bound to the document yet, don't worry, we'll get called again + // when that happens + if (!aContextNode->IsInUncomposedDoc()) + return; + + // Hold ref to the old element so that it isn't destroyed in between resetting + // the referenced element and using the pointer to update the referenced + // element. + RefPtr oldReferencedElement = mReferencedElement.get(); + + if (mParams.mDependentElemID) { + mReferencedElement.ResetWithID(aContextNode, + nsDependentAtomString(mParams.mDependentElemID)); + } else if (mParams.mType == nsSMILTimeValueSpecParams::EVENT) { + Element* target = mOwner->GetTargetElement(); + mReferencedElement.ResetWithElement(target); + } else if (mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY) { + nsIDocument* doc = aContextNode->GetUncomposedDoc(); + MOZ_ASSERT(doc, "We are in the document but current doc is null"); + mReferencedElement.ResetWithElement(doc->GetRootElement()); + } else { + MOZ_ASSERT(false, "Syncbase or repeat spec without ID"); + } + UpdateReferencedElement(oldReferencedElement, mReferencedElement.get()); +} + +bool +nsSMILTimeValueSpec::IsEventBased() const +{ + return mParams.mType == nsSMILTimeValueSpecParams::EVENT || + mParams.mType == nsSMILTimeValueSpecParams::REPEAT || + mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY; +} + +void +nsSMILTimeValueSpec::HandleNewInterval(nsSMILInterval& aInterval, + const nsSMILTimeContainer* aSrcContainer) +{ + const nsSMILInstanceTime& baseInstance = mParams.mSyncBegin + ? *aInterval.Begin() : *aInterval.End(); + nsSMILTimeValue newTime = + ConvertBetweenTimeContainers(baseInstance.Time(), aSrcContainer); + + // Apply offset + if (!ApplyOffset(newTime)) { + NS_WARNING("New time overflows nsSMILTime, ignoring"); + return; + } + + // Create the instance time and register it with the interval + RefPtr newInstance = + new nsSMILInstanceTime(newTime, nsSMILInstanceTime::SOURCE_SYNCBASE, this, + &aInterval); + mOwner->AddInstanceTime(newInstance, mIsBegin); +} + +void +nsSMILTimeValueSpec::HandleTargetElementChange(Element* aNewTarget) +{ + if (!IsEventBased() || mParams.mDependentElemID) + return; + + mReferencedElement.ResetWithElement(aNewTarget); +} + +void +nsSMILTimeValueSpec::HandleChangedInstanceTime( + const nsSMILInstanceTime& aBaseTime, + const nsSMILTimeContainer* aSrcContainer, + nsSMILInstanceTime& aInstanceTimeToUpdate, + bool aObjectChanged) +{ + // If the instance time is fixed (e.g. because it's being used as the begin + // time of an active or postactive interval) we just ignore the change. + if (aInstanceTimeToUpdate.IsFixedTime()) + return; + + nsSMILTimeValue updatedTime = + ConvertBetweenTimeContainers(aBaseTime.Time(), aSrcContainer); + + // Apply offset + if (!ApplyOffset(updatedTime)) { + NS_WARNING("Updated time overflows nsSMILTime, ignoring"); + return; + } + + // The timed element that owns the instance time does the updating so it can + // re-sort its array of instance times more efficiently + if (aInstanceTimeToUpdate.Time() != updatedTime || aObjectChanged) { + mOwner->UpdateInstanceTime(&aInstanceTimeToUpdate, updatedTime, mIsBegin); + } +} + +void +nsSMILTimeValueSpec::HandleDeletedInstanceTime( + nsSMILInstanceTime &aInstanceTime) +{ + mOwner->RemoveInstanceTime(&aInstanceTime, mIsBegin); +} + +bool +nsSMILTimeValueSpec::DependsOnBegin() const +{ + return mParams.mSyncBegin; +} + +void +nsSMILTimeValueSpec::Traverse(nsCycleCollectionTraversalCallback* aCallback) +{ + mReferencedElement.Traverse(aCallback); +} + +void +nsSMILTimeValueSpec::Unlink() +{ + UnregisterFromReferencedElement(mReferencedElement.get()); + mReferencedElement.Unlink(); +} + +//---------------------------------------------------------------------- +// Implementation helpers + +void +nsSMILTimeValueSpec::UpdateReferencedElement(Element* aFrom, Element* aTo) +{ + if (aFrom == aTo) + return; + + UnregisterFromReferencedElement(aFrom); + + switch (mParams.mType) + { + case nsSMILTimeValueSpecParams::SYNCBASE: + { + nsSMILTimedElement* to = GetTimedElement(aTo); + if (to) { + to->AddDependent(*this); + } + } + break; + + case nsSMILTimeValueSpecParams::EVENT: + case nsSMILTimeValueSpecParams::REPEAT: + case nsSMILTimeValueSpecParams::ACCESSKEY: + RegisterEventListener(aTo); + break; + + default: + // not a referencing-type + break; + } +} + +void +nsSMILTimeValueSpec::UnregisterFromReferencedElement(Element* aElement) +{ + if (!aElement) + return; + + if (mParams.mType == nsSMILTimeValueSpecParams::SYNCBASE) { + nsSMILTimedElement* timedElement = GetTimedElement(aElement); + if (timedElement) { + timedElement->RemoveDependent(*this); + } + mOwner->RemoveInstanceTimesForCreator(this, mIsBegin); + } else if (IsEventBased()) { + UnregisterEventListener(aElement); + } +} + +nsSMILTimedElement* +nsSMILTimeValueSpec::GetTimedElement(Element* aElement) +{ + return aElement && aElement->IsNodeOfType(nsINode::eANIMATION) ? + &static_cast(aElement)->TimedElement() : nullptr; +} + +// Indicates whether we're allowed to register an event-listener +// when scripting is disabled. +bool +nsSMILTimeValueSpec::IsWhitelistedEvent() +{ + // The category of (SMIL-specific) "repeat(n)" events are allowed. + if (mParams.mType == nsSMILTimeValueSpecParams::REPEAT) { + return true; + } + + // A specific list of other SMIL-related events are allowed, too. + if (mParams.mType == nsSMILTimeValueSpecParams::EVENT && + (mParams.mEventSymbol == nsGkAtoms::repeat || + mParams.mEventSymbol == nsGkAtoms::repeatEvent || + mParams.mEventSymbol == nsGkAtoms::beginEvent || + mParams.mEventSymbol == nsGkAtoms::endEvent)) { + return true; + } + + return false; +} + +void +nsSMILTimeValueSpec::RegisterEventListener(Element* aTarget) +{ + MOZ_ASSERT(IsEventBased(), + "Attempting to register event-listener for unexpected " + "nsSMILTimeValueSpec type"); + MOZ_ASSERT(mParams.mEventSymbol, + "Attempting to register event-listener but there is no event " + "name"); + + if (!aTarget) + return; + + // When script is disabled, only allow registration for whitelisted events. + if (!aTarget->GetOwnerDocument()->IsScriptEnabled() && + !IsWhitelistedEvent()) { + return; + } + + if (!mEventListener) { + mEventListener = new EventListener(this); + } + + EventListenerManager* elm = GetEventListenerManager(aTarget); + if (!elm) + return; + + elm->AddEventListenerByType(mEventListener, + nsDependentAtomString(mParams.mEventSymbol), + AllEventsAtSystemGroupBubble()); +} + +void +nsSMILTimeValueSpec::UnregisterEventListener(Element* aTarget) +{ + if (!aTarget || !mEventListener) + return; + + EventListenerManager* elm = GetEventListenerManager(aTarget); + if (!elm) + return; + + elm->RemoveEventListenerByType(mEventListener, + nsDependentAtomString(mParams.mEventSymbol), + AllEventsAtSystemGroupBubble()); +} + +EventListenerManager* +nsSMILTimeValueSpec::GetEventListenerManager(Element* aTarget) +{ + MOZ_ASSERT(aTarget, "null target; can't get EventListenerManager"); + + nsCOMPtr target; + + if (mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY) { + nsIDocument* doc = aTarget->GetUncomposedDoc(); + if (!doc) + return nullptr; + nsPIDOMWindowOuter* win = doc->GetWindow(); + if (!win) + return nullptr; + target = do_QueryInterface(win); + } else { + target = aTarget; + } + if (!target) + return nullptr; + + return target->GetOrCreateListenerManager(); +} + +void +nsSMILTimeValueSpec::HandleEvent(nsIDOMEvent* aEvent) +{ + MOZ_ASSERT(mEventListener, "Got event without an event listener"); + MOZ_ASSERT(IsEventBased(), + "Got event for non-event nsSMILTimeValueSpec"); + MOZ_ASSERT(aEvent, "No event supplied"); + + // XXX In the long run we should get the time from the event itself which will + // store the time in global document time which we'll need to convert to our + // time container + nsSMILTimeContainer* container = mOwner->GetTimeContainer(); + if (!container) + return; + + if (!CheckEventDetail(aEvent)) + return; + + nsSMILTime currentTime = container->GetCurrentTime(); + nsSMILTimeValue newTime(currentTime); + if (!ApplyOffset(newTime)) { + NS_WARNING("New time generated from event overflows nsSMILTime, ignoring"); + return; + } + + RefPtr newInstance = + new nsSMILInstanceTime(newTime, nsSMILInstanceTime::SOURCE_EVENT); + mOwner->AddInstanceTime(newInstance, mIsBegin); +} + +bool +nsSMILTimeValueSpec::CheckEventDetail(nsIDOMEvent *aEvent) +{ + switch (mParams.mType) + { + case nsSMILTimeValueSpecParams::REPEAT: + return CheckRepeatEventDetail(aEvent); + + case nsSMILTimeValueSpecParams::ACCESSKEY: + return CheckAccessKeyEventDetail(aEvent); + + default: + // nothing to check + return true; + } +} + +bool +nsSMILTimeValueSpec::CheckRepeatEventDetail(nsIDOMEvent *aEvent) +{ + nsCOMPtr timeEvent = do_QueryInterface(aEvent); + if (!timeEvent) { + NS_WARNING("Received a repeat event that was not a DOMTimeEvent"); + return false; + } + + int32_t detail; + timeEvent->GetDetail(&detail); + return detail > 0 && (uint32_t)detail == mParams.mRepeatIterationOrAccessKey; +} + +bool +nsSMILTimeValueSpec::CheckAccessKeyEventDetail(nsIDOMEvent *aEvent) +{ + nsCOMPtr keyEvent = do_QueryInterface(aEvent); + if (!keyEvent) { + NS_WARNING("Received an accesskey event that was not a DOMKeyEvent"); + return false; + } + + // Ignore the key event if any modifier keys are pressed UNLESS we're matching + // on the charCode in which case we ignore the state of the shift and alt keys + // since they might be needed to generate the character in question. + bool isCtrl; + bool isMeta; + keyEvent->GetCtrlKey(&isCtrl); + keyEvent->GetMetaKey(&isMeta); + if (isCtrl || isMeta) + return false; + + uint32_t code; + keyEvent->GetCharCode(&code); + if (code) + return code == mParams.mRepeatIterationOrAccessKey; + + // Only match on the keyCode if it corresponds to some ASCII character that + // does not produce a charCode. + // In this case we can safely bail out if either alt or shift is pressed since + // they won't already be incorporated into the keyCode unlike the charCode. + bool isAlt; + bool isShift; + keyEvent->GetAltKey(&isAlt); + keyEvent->GetShiftKey(&isShift); + if (isAlt || isShift) + return false; + + keyEvent->GetKeyCode(&code); + switch (code) + { + case nsIDOMKeyEvent::DOM_VK_BACK_SPACE: + return mParams.mRepeatIterationOrAccessKey == 0x08; + + case nsIDOMKeyEvent::DOM_VK_RETURN: + return mParams.mRepeatIterationOrAccessKey == 0x0A || + mParams.mRepeatIterationOrAccessKey == 0x0D; + + case nsIDOMKeyEvent::DOM_VK_ESCAPE: + return mParams.mRepeatIterationOrAccessKey == 0x1B; + + case nsIDOMKeyEvent::DOM_VK_DELETE: + return mParams.mRepeatIterationOrAccessKey == 0x7F; + + default: + return false; + } +} + +nsSMILTimeValue +nsSMILTimeValueSpec::ConvertBetweenTimeContainers( + const nsSMILTimeValue& aSrcTime, + const nsSMILTimeContainer* aSrcContainer) +{ + // If the source time is either indefinite or unresolved the result is going + // to be the same + if (!aSrcTime.IsDefinite()) + return aSrcTime; + + // Convert from source time container to our parent time container + const nsSMILTimeContainer* dstContainer = mOwner->GetTimeContainer(); + if (dstContainer == aSrcContainer) + return aSrcTime; + + // If one of the elements is not attached to a time container then we can't do + // any meaningful conversion + if (!aSrcContainer || !dstContainer) + return nsSMILTimeValue(); // unresolved + + nsSMILTimeValue docTime = + aSrcContainer->ContainerToParentTime(aSrcTime.GetMillis()); + + if (docTime.IsIndefinite()) + // This will happen if the source container is paused and we have a future + // time. Just return the indefinite time. + return docTime; + + MOZ_ASSERT(docTime.IsDefinite(), + "ContainerToParentTime gave us an unresolved or indefinite time"); + + return dstContainer->ParentToContainerTime(docTime.GetMillis()); +} + +bool +nsSMILTimeValueSpec::ApplyOffset(nsSMILTimeValue& aTime) const +{ + // indefinite + offset = indefinite. Likewise for unresolved times. + if (!aTime.IsDefinite()) { + return true; + } + + double resultAsDouble = + (double)aTime.GetMillis() + mParams.mOffset.GetMillis(); + if (resultAsDouble > std::numeric_limits::max() || + resultAsDouble < std::numeric_limits::min()) { + return false; + } + aTime.SetMillis(aTime.GetMillis() + mParams.mOffset.GetMillis()); + return true; +} diff --git a/dom/smil/nsSMILTimeValueSpec.h b/dom/smil/nsSMILTimeValueSpec.h new file mode 100644 index 000000000..d0817c15f --- /dev/null +++ b/dom/smil/nsSMILTimeValueSpec.h @@ -0,0 +1,131 @@ +/* -*- 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/. */ + +#ifndef NS_SMILTIMEVALUESPEC_H_ +#define NS_SMILTIMEVALUESPEC_H_ + +#include "mozilla/Attributes.h" +#include "nsSMILTimeValueSpecParams.h" +#include "nsReferencedElement.h" +#include "nsIDOMEventListener.h" + +class nsAString; +class nsSMILTimeValue; +class nsSMILTimedElement; +class nsSMILTimeContainer; +class nsSMILInstanceTime; +class nsSMILInterval; + +namespace mozilla { +class EventListenerManager; +} // namespace mozilla + +//---------------------------------------------------------------------- +// nsSMILTimeValueSpec class +// +// An individual element of a 'begin' or 'end' attribute, e.g. '5s', 'a.end'. +// This class handles the parsing of such specifications and performs the +// necessary event handling (for event, repeat, and accesskey specifications) +// and synchronisation (for syncbase specifications). +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in nsSMILTimeValue.h + +class nsSMILTimeValueSpec +{ +public: + typedef mozilla::dom::Element Element; + + nsSMILTimeValueSpec(nsSMILTimedElement& aOwner, bool aIsBegin); + ~nsSMILTimeValueSpec(); + + nsresult SetSpec(const nsAString& aStringSpec, Element* aContextNode); + void ResolveReferences(nsIContent* aContextNode); + bool IsEventBased() const; + + void HandleNewInterval(nsSMILInterval& aInterval, + const nsSMILTimeContainer* aSrcContainer); + void HandleTargetElementChange(Element* aNewTarget); + + // For created nsSMILInstanceTime objects + bool DependsOnBegin() const; + void HandleChangedInstanceTime(const nsSMILInstanceTime& aBaseTime, + const nsSMILTimeContainer* aSrcContainer, + nsSMILInstanceTime& aInstanceTimeToUpdate, + bool aObjectChanged); + void HandleDeletedInstanceTime(nsSMILInstanceTime& aInstanceTime); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + +protected: + void UpdateReferencedElement(Element* aFrom, Element* aTo); + void UnregisterFromReferencedElement(Element* aElement); + nsSMILTimedElement* GetTimedElement(Element* aElement); + bool IsWhitelistedEvent(); + void RegisterEventListener(Element* aElement); + void UnregisterEventListener(Element* aElement); + mozilla::EventListenerManager* GetEventListenerManager(Element* aElement); + void HandleEvent(nsIDOMEvent* aEvent); + bool CheckEventDetail(nsIDOMEvent* aEvent); + bool CheckRepeatEventDetail(nsIDOMEvent* aEvent); + bool CheckAccessKeyEventDetail(nsIDOMEvent* aEvent); + nsSMILTimeValue ConvertBetweenTimeContainers(const nsSMILTimeValue& aSrcTime, + const nsSMILTimeContainer* aSrcContainer); + bool ApplyOffset(nsSMILTimeValue& aTime) const; + + nsSMILTimedElement* mOwner; + bool mIsBegin; // Indicates if *we* are a begin spec, + // not to be confused with + // mParams.mSyncBegin which indicates + // if we're synced with the begin of + // the target. + nsSMILTimeValueSpecParams mParams; + + class TimeReferenceElement : public nsReferencedElement + { + public: + explicit TimeReferenceElement(nsSMILTimeValueSpec* aOwner) : mSpec(aOwner) { } + void ResetWithElement(Element* aTo) { + RefPtr from = get(); + Unlink(); + ElementChanged(from, aTo); + } + + protected: + virtual void ElementChanged(Element* aFrom, Element* aTo) override + { + nsReferencedElement::ElementChanged(aFrom, aTo); + mSpec->UpdateReferencedElement(aFrom, aTo); + } + virtual bool IsPersistent() override { return true; } + private: + nsSMILTimeValueSpec* mSpec; + }; + + TimeReferenceElement mReferencedElement; + + class EventListener final : public nsIDOMEventListener + { + ~EventListener() {} + public: + explicit EventListener(nsSMILTimeValueSpec* aOwner) : mSpec(aOwner) { } + void Disconnect() + { + mSpec = nullptr; + } + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + private: + nsSMILTimeValueSpec* mSpec; + }; + RefPtr mEventListener; +}; + +#endif // NS_SMILTIMEVALUESPEC_H_ diff --git a/dom/smil/nsSMILTimeValueSpecParams.h b/dom/smil/nsSMILTimeValueSpecParams.h new file mode 100644 index 000000000..7a5ada626 --- /dev/null +++ b/dom/smil/nsSMILTimeValueSpecParams.h @@ -0,0 +1,68 @@ +/* -*- 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/. */ + +#ifndef NS_SMILTIMEVALUESPECPARAMS_H_ +#define NS_SMILTIMEVALUESPECPARAMS_H_ + +#include "nsSMILTimeValue.h" +#include "nsIAtom.h" + +//---------------------------------------------------------------------- +// nsSMILTimeValueSpecParams +// +// A simple data type for storing the result of parsing a single begin or end +// value (e.g. the '5s' in begin="5s; indefinite; a.begin+2s"). + +class nsSMILTimeValueSpecParams +{ +public: + nsSMILTimeValueSpecParams() + : + mType(INDEFINITE), + mSyncBegin(false), + mRepeatIterationOrAccessKey(0) + { } + + // The type of value this specification describes + enum { + OFFSET, + SYNCBASE, + EVENT, + REPEAT, + ACCESSKEY, + WALLCLOCK, + INDEFINITE + } mType; + + // A clock value that is added to: + // - type OFFSET: the document begin + // - type SYNCBASE: the timebase's begin or end time + // - type EVENT: the event time + // - type REPEAT: the repeat time + // - type ACCESSKEY: the keypress time + // It is not used for WALLCLOCK or INDEFINITE times + nsSMILTimeValue mOffset; + + // The base element that this specification refers to. + // For SYNCBASE types, this is the timebase + // For EVENT and REPEAT types, this is the eventbase + RefPtr mDependentElemID; + + // The event to respond to. + // Only used for EVENT types. + RefPtr mEventSymbol; + + // Indicates if this specification refers to the begin or end of the dependent + // element. + // Only used for SYNCBASE types. + bool mSyncBegin; + + // The repeat iteration (type=REPEAT) or access key (type=ACCESSKEY) to + // respond to. + uint32_t mRepeatIterationOrAccessKey; +}; + +#endif // NS_SMILTIMEVALUESPECPARAMS_H_ diff --git a/dom/smil/nsSMILTimedElement.cpp b/dom/smil/nsSMILTimedElement.cpp new file mode 100644 index 000000000..120536be0 --- /dev/null +++ b/dom/smil/nsSMILTimedElement.cpp @@ -0,0 +1,2444 @@ +/* -*- 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 "mozilla/DebugOnly.h" + +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsAutoPtr.h" +#include "nsSMILTimedElement.h" +#include "nsAttrValueInlines.h" +#include "nsSMILAnimationFunction.h" +#include "nsSMILTimeValue.h" +#include "nsSMILTimeValueSpec.h" +#include "nsSMILInstanceTime.h" +#include "nsSMILParserUtils.h" +#include "nsSMILTimeContainer.h" +#include "nsGkAtoms.h" +#include "nsReadableUtils.h" +#include "nsMathUtils.h" +#include "nsThreadUtils.h" +#include "nsIPresShell.h" +#include "prdtoa.h" +#include "plstr.h" +#include "prtime.h" +#include "nsString.h" +#include "mozilla/AutoRestore.h" +#include "nsCharSeparatedTokenizer.h" +#include + +using namespace mozilla; +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// Helper class: InstanceTimeComparator + +// Upon inserting an instance time into one of our instance time lists we assign +// it a serial number. This allows us to sort the instance times in such a way +// that where we have several equal instance times, the ones added later will +// sort later. This means that when we call UpdateCurrentInterval during the +// waiting state we won't unnecessarily change the begin instance. +// +// The serial number also means that every instance time has an unambiguous +// position in the array so we can use RemoveElementSorted and the like. +bool +nsSMILTimedElement::InstanceTimeComparator::Equals( + const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const +{ + MOZ_ASSERT(aElem1 && aElem2, + "Trying to compare null instance time pointers"); + MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(), + "Instance times have not been assigned serial numbers"); + MOZ_ASSERT(aElem1 == aElem2 || aElem1->Serial() != aElem2->Serial(), + "Serial numbers are not unique"); + + return aElem1->Serial() == aElem2->Serial(); +} + +bool +nsSMILTimedElement::InstanceTimeComparator::LessThan( + const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const +{ + MOZ_ASSERT(aElem1 && aElem2, + "Trying to compare null instance time pointers"); + MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(), + "Instance times have not been assigned serial numbers"); + + int8_t cmp = aElem1->Time().CompareTo(aElem2->Time()); + return cmp == 0 ? aElem1->Serial() < aElem2->Serial() : cmp < 0; +} + +//---------------------------------------------------------------------- +// Helper class: AsyncTimeEventRunner + +namespace +{ + class AsyncTimeEventRunner : public Runnable + { + protected: + RefPtr mTarget; + EventMessage mMsg; + int32_t mDetail; + + public: + AsyncTimeEventRunner(nsIContent* aTarget, EventMessage aMsg, + int32_t aDetail) + : mTarget(aTarget) + , mMsg(aMsg) + , mDetail(aDetail) + { + } + + NS_IMETHOD Run() override + { + InternalSMILTimeEvent event(true, mMsg); + event.mDetail = mDetail; + + nsPresContext* context = nullptr; + nsIDocument* doc = mTarget->GetUncomposedDoc(); + if (doc) { + nsCOMPtr shell = doc->GetShell(); + if (shell) { + context = shell->GetPresContext(); + } + } + + return EventDispatcher::Dispatch(mTarget, context, &event); + } + }; +} // namespace + +//---------------------------------------------------------------------- +// Helper class: AutoIntervalUpdateBatcher + +// Stack-based helper class to set the mDeferIntervalUpdates flag on an +// nsSMILTimedElement and perform the UpdateCurrentInterval when the object is +// destroyed. +// +// If several of these objects are allocated on the stack, the update will not +// be performed until the last object for a given nsSMILTimedElement is +// destroyed. +class MOZ_STACK_CLASS nsSMILTimedElement::AutoIntervalUpdateBatcher +{ +public: + explicit AutoIntervalUpdateBatcher(nsSMILTimedElement& aTimedElement) + : mTimedElement(aTimedElement), + mDidSetFlag(!aTimedElement.mDeferIntervalUpdates) + { + mTimedElement.mDeferIntervalUpdates = true; + } + + ~AutoIntervalUpdateBatcher() + { + if (!mDidSetFlag) + return; + + mTimedElement.mDeferIntervalUpdates = false; + + if (mTimedElement.mDoDeferredUpdate) { + mTimedElement.mDoDeferredUpdate = false; + mTimedElement.UpdateCurrentInterval(); + } + } + +private: + nsSMILTimedElement& mTimedElement; + bool mDidSetFlag; +}; + +//---------------------------------------------------------------------- +// Helper class: AutoIntervalUpdater + +// Stack-based helper class to call UpdateCurrentInterval when it is destroyed +// which helps avoid bugs where we forget to call UpdateCurrentInterval in the +// case of early returns (e.g. due to parse errors). +// +// This can be safely used in conjunction with AutoIntervalUpdateBatcher; any +// calls to UpdateCurrentInterval made by this class will simply be deferred if +// there is an AutoIntervalUpdateBatcher on the stack. +class MOZ_STACK_CLASS nsSMILTimedElement::AutoIntervalUpdater +{ +public: + explicit AutoIntervalUpdater(nsSMILTimedElement& aTimedElement) + : mTimedElement(aTimedElement) { } + + ~AutoIntervalUpdater() + { + mTimedElement.UpdateCurrentInterval(); + } + +private: + nsSMILTimedElement& mTimedElement; +}; + +//---------------------------------------------------------------------- +// Templated helper functions + +// Selectively remove elements from an array of type +// nsTArray > with O(n) performance. +template +void +nsSMILTimedElement::RemoveInstanceTimes(InstanceTimeList& aArray, + TestFunctor& aTest) +{ + InstanceTimeList newArray; + for (uint32_t i = 0; i < aArray.Length(); ++i) { + nsSMILInstanceTime* item = aArray[i].get(); + if (aTest(item, i)) { + // As per bugs 665334 and 669225 we should be careful not to remove the + // instance time that corresponds to the previous interval's end time. + // + // Most functors supplied here fulfil this condition by checking if the + // instance time is marked as "ShouldPreserve" and if so, not deleting it. + // + // However, when filtering instance times, we sometimes need to drop even + // instance times marked as "ShouldPreserve". In that case we take special + // care not to delete the end instance time of the previous interval. + MOZ_ASSERT(!GetPreviousInterval() || item != GetPreviousInterval()->End(), + "Removing end instance time of previous interval"); + item->Unlink(); + } else { + newArray.AppendElement(item); + } + } + aArray.Clear(); + aArray.SwapElements(newArray); +} + +//---------------------------------------------------------------------- +// Static members + +nsAttrValue::EnumTable nsSMILTimedElement::sFillModeTable[] = { + {"remove", FILL_REMOVE}, + {"freeze", FILL_FREEZE}, + {nullptr, 0} +}; + +nsAttrValue::EnumTable nsSMILTimedElement::sRestartModeTable[] = { + {"always", RESTART_ALWAYS}, + {"whenNotActive", RESTART_WHENNOTACTIVE}, + {"never", RESTART_NEVER}, + {nullptr, 0} +}; + +const nsSMILMilestone nsSMILTimedElement::sMaxMilestone(INT64_MAX, false); + +// The thresholds at which point we start filtering intervals and instance times +// indiscriminately. +// See FilterIntervals and FilterInstanceTimes. +const uint8_t nsSMILTimedElement::sMaxNumIntervals = 20; +const uint8_t nsSMILTimedElement::sMaxNumInstanceTimes = 100; + +// Detect if we arrive in some sort of undetected recursive syncbase dependency +// relationship +const uint8_t nsSMILTimedElement::sMaxUpdateIntervalRecursionDepth = 20; + +//---------------------------------------------------------------------- +// Ctor, dtor + +nsSMILTimedElement::nsSMILTimedElement() +: + mAnimationElement(nullptr), + mFillMode(FILL_REMOVE), + mRestartMode(RESTART_ALWAYS), + mInstanceSerialIndex(0), + mClient(nullptr), + mCurrentInterval(nullptr), + mCurrentRepeatIteration(0), + mPrevRegisteredMilestone(sMaxMilestone), + mElementState(STATE_STARTUP), + mSeekState(SEEK_NOT_SEEKING), + mDeferIntervalUpdates(false), + mDoDeferredUpdate(false), + mIsDisabled(false), + mDeleteCount(0), + mUpdateIntervalRecursionDepth(0) +{ + mSimpleDur.SetIndefinite(); + mMin.SetMillis(0L); + mMax.SetIndefinite(); +} + +nsSMILTimedElement::~nsSMILTimedElement() +{ + // Unlink all instance times from dependent intervals + for (uint32_t i = 0; i < mBeginInstances.Length(); ++i) { + mBeginInstances[i]->Unlink(); + } + mBeginInstances.Clear(); + for (uint32_t i = 0; i < mEndInstances.Length(); ++i) { + mEndInstances[i]->Unlink(); + } + mEndInstances.Clear(); + + // Notify anyone listening to our intervals that they're gone + // (We shouldn't get any callbacks from this because all our instance times + // are now disassociated with any intervals) + ClearIntervals(); + + // The following assertions are important in their own right (for checking + // correct behavior) but also because AutoIntervalUpdateBatcher holds pointers + // to class so if they fail there's the possibility we might have dangling + // pointers. + MOZ_ASSERT(!mDeferIntervalUpdates, + "Interval updates should no longer be blocked when an " + "nsSMILTimedElement disappears"); + MOZ_ASSERT(!mDoDeferredUpdate, + "There should no longer be any pending updates when an " + "nsSMILTimedElement disappears"); +} + +void +nsSMILTimedElement::SetAnimationElement(SVGAnimationElement* aElement) +{ + MOZ_ASSERT(aElement, "NULL owner element"); + MOZ_ASSERT(!mAnimationElement, "Re-setting owner"); + mAnimationElement = aElement; +} + +nsSMILTimeContainer* +nsSMILTimedElement::GetTimeContainer() +{ + return mAnimationElement ? mAnimationElement->GetTimeContainer() : nullptr; +} + +dom::Element* +nsSMILTimedElement::GetTargetElement() +{ + return mAnimationElement ? + mAnimationElement->GetTargetElementContent() : + nullptr; +} + +//---------------------------------------------------------------------- +// nsIDOMElementTimeControl methods +// +// The definition of the ElementTimeControl interface differs between SMIL +// Animation and SVG 1.1. In SMIL Animation all methods have a void return +// type and the new instance time is simply added to the list and restart +// semantics are applied as with any other instance time. In the SVG definition +// the methods return a bool depending on the restart mode. +// +// This inconsistency has now been addressed by an erratum in SVG 1.1: +// +// http://www.w3.org/2003/01/REC-SVG11-20030114-errata#elementtimecontrol-interface +// +// which favours the definition in SMIL, i.e. instance times are just added +// without first checking the restart mode. + +nsresult +nsSMILTimedElement::BeginElementAt(double aOffsetSeconds) +{ + nsSMILTimeContainer* container = GetTimeContainer(); + if (!container) + return NS_ERROR_FAILURE; + + nsSMILTime currentTime = container->GetCurrentTime(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, true); +} + +nsresult +nsSMILTimedElement::EndElementAt(double aOffsetSeconds) +{ + nsSMILTimeContainer* container = GetTimeContainer(); + if (!container) + return NS_ERROR_FAILURE; + + nsSMILTime currentTime = container->GetCurrentTime(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, false); +} + +//---------------------------------------------------------------------- +// nsSVGAnimationElement methods + +nsSMILTimeValue +nsSMILTimedElement::GetStartTime() const +{ + return mElementState == STATE_WAITING || mElementState == STATE_ACTIVE + ? mCurrentInterval->Begin()->Time() + : nsSMILTimeValue(); +} + +//---------------------------------------------------------------------- +// Hyperlinking support + +nsSMILTimeValue +nsSMILTimedElement::GetHyperlinkTime() const +{ + nsSMILTimeValue hyperlinkTime; // Default ctor creates unresolved time + + if (mElementState == STATE_ACTIVE) { + hyperlinkTime = mCurrentInterval->Begin()->Time(); + } else if (!mBeginInstances.IsEmpty()) { + hyperlinkTime = mBeginInstances[0]->Time(); + } + + return hyperlinkTime; +} + +//---------------------------------------------------------------------- +// nsSMILTimedElement + +void +nsSMILTimedElement::AddInstanceTime(nsSMILInstanceTime* aInstanceTime, + bool aIsBegin) +{ + MOZ_ASSERT(aInstanceTime, "Attempting to add null instance time"); + + // Event-sensitivity: If an element is not active (but the parent time + // container is), then events are only handled for begin specifications. + if (mElementState != STATE_ACTIVE && !aIsBegin && + aInstanceTime->IsDynamic()) + { + // No need to call Unlink here--dynamic instance times shouldn't be linked + // to anything that's going to miss them + MOZ_ASSERT(!aInstanceTime->GetBaseInterval(), + "Dynamic instance time has a base interval--we probably need " + "to unlink it if we're not going to use it"); + return; + } + + aInstanceTime->SetSerial(++mInstanceSerialIndex); + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + RefPtr* inserted = + instanceList.InsertElementSorted(aInstanceTime, InstanceTimeComparator()); + if (!inserted) { + NS_WARNING("Insufficient memory to insert instance time"); + return; + } + + UpdateCurrentInterval(); +} + +void +nsSMILTimedElement::UpdateInstanceTime(nsSMILInstanceTime* aInstanceTime, + nsSMILTimeValue& aUpdatedTime, + bool aIsBegin) +{ + MOZ_ASSERT(aInstanceTime, "Attempting to update null instance time"); + + // The reason we update the time here and not in the nsSMILTimeValueSpec is + // that it means we *could* re-sort more efficiently by doing a sorted remove + // and insert but currently this doesn't seem to be necessary given how + // infrequently we get these change notices. + aInstanceTime->DependentUpdate(aUpdatedTime); + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + instanceList.Sort(InstanceTimeComparator()); + + // Generally speaking, UpdateCurrentInterval makes changes to the current + // interval and sends changes notices itself. However, in this case because + // instance times are shared between the instance time list and the intervals + // we are effectively changing the current interval outside + // UpdateCurrentInterval so we need to explicitly signal that we've made + // a change. + // + // This wouldn't be necessary if we cloned instance times on adding them to + // the current interval but this introduces other complications (particularly + // detecting which instance time is being used to define the begin of the + // current interval when doing a Reset). + bool changedCurrentInterval = mCurrentInterval && + (mCurrentInterval->Begin() == aInstanceTime || + mCurrentInterval->End() == aInstanceTime); + + UpdateCurrentInterval(changedCurrentInterval); +} + +void +nsSMILTimedElement::RemoveInstanceTime(nsSMILInstanceTime* aInstanceTime, + bool aIsBegin) +{ + MOZ_ASSERT(aInstanceTime, "Attempting to remove null instance time"); + + // If the instance time should be kept (because it is or was the fixed end + // point of an interval) then just disassociate it from the creator. + if (aInstanceTime->ShouldPreserve()) { + aInstanceTime->Unlink(); + return; + } + + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + mozilla::DebugOnly found = + instanceList.RemoveElementSorted(aInstanceTime, InstanceTimeComparator()); + MOZ_ASSERT(found, "Couldn't find instance time to delete"); + + UpdateCurrentInterval(); +} + +namespace +{ + class MOZ_STACK_CLASS RemoveByCreator + { + public: + explicit RemoveByCreator(const nsSMILTimeValueSpec* aCreator) : mCreator(aCreator) + { } + + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + if (aInstanceTime->GetCreator() != mCreator) + return false; + + // If the instance time should be kept (because it is or was the fixed end + // point of an interval) then just disassociate it from the creator. + if (aInstanceTime->ShouldPreserve()) { + aInstanceTime->Unlink(); + return false; + } + + return true; + } + + private: + const nsSMILTimeValueSpec* mCreator; + }; +} // namespace + +void +nsSMILTimedElement::RemoveInstanceTimesForCreator( + const nsSMILTimeValueSpec* aCreator, bool aIsBegin) +{ + MOZ_ASSERT(aCreator, "Creator not set"); + + InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances; + RemoveByCreator removeByCreator(aCreator); + RemoveInstanceTimes(instances, removeByCreator); + + UpdateCurrentInterval(); +} + +void +nsSMILTimedElement::SetTimeClient(nsSMILAnimationFunction* aClient) +{ + // + // No need to check for nullptr. A nullptr parameter simply means to remove the + // previous client which we do by setting to nullptr anyway. + // + + mClient = aClient; +} + +void +nsSMILTimedElement::SampleAt(nsSMILTime aContainerTime) +{ + if (mIsDisabled) + return; + + // Milestones are cleared before a sample + mPrevRegisteredMilestone = sMaxMilestone; + + DoSampleAt(aContainerTime, false); +} + +void +nsSMILTimedElement::SampleEndAt(nsSMILTime aContainerTime) +{ + if (mIsDisabled) + return; + + // Milestones are cleared before a sample + mPrevRegisteredMilestone = sMaxMilestone; + + // If the current interval changes, we don't bother trying to remove any old + // milestones we'd registered. So it's possible to get a call here to end an + // interval at a time that no longer reflects the end of the current interval. + // + // For now we just check that we're actually in an interval but note that the + // initial sample we use to initialise the model is an end sample. This is + // because we want to resolve all the instance times before committing to an + // initial interval. Therefore an end sample from the startup state is also + // acceptable. + if (mElementState == STATE_ACTIVE || mElementState == STATE_STARTUP) { + DoSampleAt(aContainerTime, true); // End sample + } else { + // Even if this was an unnecessary milestone sample we want to be sure that + // our next real milestone is registered. + RegisterMilestone(); + } +} + +void +nsSMILTimedElement::DoSampleAt(nsSMILTime aContainerTime, bool aEndOnly) +{ + MOZ_ASSERT(mAnimationElement, + "Got sample before being registered with an animation element"); + MOZ_ASSERT(GetTimeContainer(), + "Got sample without being registered with a time container"); + + // This could probably happen if we later implement externalResourcesRequired + // (bug 277955) and whilst waiting for those resources (and the animation to + // start) we transfer a node from another document fragment that has already + // started. In such a case we might receive milestone samples registered with + // the already active container. + if (GetTimeContainer()->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) + return; + + // We use an end-sample to start animation since an end-sample lets us + // tentatively create an interval without committing to it (by transitioning + // to the ACTIVE state) and this is necessary because we might have + // dependencies on other animations that are yet to start. After these + // other animations start, it may be necessary to revise our initial interval. + // + // However, sometimes instead of an end-sample we can get a regular sample + // during STARTUP state. This can happen, for example, if we register + // a milestone before time t=0 and are then re-bound to the tree (which sends + // us back to the STARTUP state). In such a case we should just ignore the + // sample and wait for our real initial sample which will be an end-sample. + if (mElementState == STATE_STARTUP && !aEndOnly) + return; + + bool finishedSeek = false; + if (GetTimeContainer()->IsSeeking() && mSeekState == SEEK_NOT_SEEKING) { + mSeekState = mElementState == STATE_ACTIVE ? + SEEK_FORWARD_FROM_ACTIVE : + SEEK_FORWARD_FROM_INACTIVE; + } else if (mSeekState != SEEK_NOT_SEEKING && + !GetTimeContainer()->IsSeeking()) { + finishedSeek = true; + } + + bool stateChanged; + nsSMILTimeValue sampleTime(aContainerTime); + + do { +#ifdef DEBUG + // Check invariant + if (mElementState == STATE_STARTUP || mElementState == STATE_POSTACTIVE) { + MOZ_ASSERT(!mCurrentInterval, + "Shouldn't have current interval in startup or postactive " + "states"); + } else { + MOZ_ASSERT(mCurrentInterval, + "Should have current interval in waiting and active states"); + } +#endif + + stateChanged = false; + + switch (mElementState) + { + case STATE_STARTUP: + { + nsSMILInterval firstInterval; + mElementState = GetNextInterval(nullptr, nullptr, nullptr, firstInterval) + ? STATE_WAITING + : STATE_POSTACTIVE; + stateChanged = true; + if (mElementState == STATE_WAITING) { + mCurrentInterval = new nsSMILInterval(firstInterval); + NotifyNewInterval(); + } + } + break; + + case STATE_WAITING: + { + if (mCurrentInterval->Begin()->Time() <= sampleTime) { + mElementState = STATE_ACTIVE; + mCurrentInterval->FixBegin(); + if (mClient) { + mClient->Activate(mCurrentInterval->Begin()->Time().GetMillis()); + } + if (mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILBeginEvent, 0); + } + if (HasPlayed()) { + Reset(); // Apply restart behaviour + // The call to Reset() may mean that the end point of our current + // interval should be changed and so we should update the interval + // now. However, calling UpdateCurrentInterval could result in the + // interval getting deleted (perhaps through some web of syncbase + // dependencies) therefore we make updating the interval the last + // thing we do. There is no guarantee that mCurrentInterval is set + // after this. + UpdateCurrentInterval(); + } + stateChanged = true; + } + } + break; + + case STATE_ACTIVE: + { + // Ending early will change the interval but we don't notify dependents + // of the change until we have closed off the current interval (since we + // don't want dependencies to un-end our early end). + bool didApplyEarlyEnd = ApplyEarlyEnd(sampleTime); + + if (mCurrentInterval->End()->Time() <= sampleTime) { + nsSMILInterval newInterval; + mElementState = + GetNextInterval(mCurrentInterval, nullptr, nullptr, newInterval) + ? STATE_WAITING + : STATE_POSTACTIVE; + if (mClient) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + } + mCurrentInterval->FixEnd(); + if (mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILEndEvent, 0); + } + mCurrentRepeatIteration = 0; + mOldIntervals.AppendElement(mCurrentInterval.forget()); + SampleFillValue(); + if (mElementState == STATE_WAITING) { + mCurrentInterval = new nsSMILInterval(newInterval); + } + // We are now in a consistent state to dispatch notifications + if (didApplyEarlyEnd) { + NotifyChangedInterval( + mOldIntervals[mOldIntervals.Length() - 1], false, true); + } + if (mElementState == STATE_WAITING) { + NotifyNewInterval(); + } + FilterHistory(); + stateChanged = true; + } else { + MOZ_ASSERT(!didApplyEarlyEnd, + "We got an early end, but didn't end"); + nsSMILTime beginTime = mCurrentInterval->Begin()->Time().GetMillis(); + NS_ASSERTION(aContainerTime >= beginTime, + "Sample time should not precede current interval"); + nsSMILTime activeTime = aContainerTime - beginTime; + + // The 'min' attribute can cause the active interval to be longer than + // the 'repeating interval'. + // In that extended period we apply the fill mode. + if (GetRepeatDuration() <= nsSMILTimeValue(activeTime)) { + if (mClient && mClient->IsActive()) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + } + SampleFillValue(); + } else { + SampleSimpleTime(activeTime); + + // We register our repeat times as milestones (except when we're + // seeking) so we should get a sample at exactly the time we repeat. + // (And even when we are seeking we want to update + // mCurrentRepeatIteration so we do that first before testing the + // seek state.) + uint32_t prevRepeatIteration = mCurrentRepeatIteration; + if ( + ActiveTimeToSimpleTime(activeTime, mCurrentRepeatIteration)==0 && + mCurrentRepeatIteration != prevRepeatIteration && + mCurrentRepeatIteration && + mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILRepeatEvent, + static_cast(mCurrentRepeatIteration)); + } + } + } + } + break; + + case STATE_POSTACTIVE: + break; + } + + // Generally we continue driving the state machine so long as we have changed + // state. However, for end samples we only drive the state machine as far as + // the waiting or postactive state because we don't want to commit to any new + // interval (by transitioning to the active state) until all the end samples + // have finished and we then have complete information about the available + // instance times upon which to base our next interval. + } while (stateChanged && (!aEndOnly || (mElementState != STATE_WAITING && + mElementState != STATE_POSTACTIVE))); + + if (finishedSeek) { + DoPostSeek(); + } + RegisterMilestone(); +} + +void +nsSMILTimedElement::HandleContainerTimeChange() +{ + // In future we could possibly introduce a separate change notice for time + // container changes and only notify those dependents who live in other time + // containers. For now we don't bother because when we re-resolve the time in + // the nsSMILTimeValueSpec we'll check if anything has changed and if not, we + // won't go any further. + if (mElementState == STATE_WAITING || mElementState == STATE_ACTIVE) { + NotifyChangedInterval(mCurrentInterval, false, false); + } +} + +namespace +{ + bool + RemoveNonDynamic(nsSMILInstanceTime* aInstanceTime) + { + // Generally dynamically-generated instance times (DOM calls, event-based + // times) are not associated with their creator nsSMILTimeValueSpec since + // they may outlive them. + MOZ_ASSERT(!aInstanceTime->IsDynamic() || !aInstanceTime->GetCreator(), + "Dynamic instance time should be unlinked from its creator"); + return !aInstanceTime->IsDynamic() && !aInstanceTime->ShouldPreserve(); + } +} // namespace + +void +nsSMILTimedElement::Rewind() +{ + MOZ_ASSERT(mAnimationElement, + "Got rewind request before being attached to an animation " + "element"); + + // It's possible to get a rewind request whilst we're already in the middle of + // a backwards seek. This can happen when we're performing tree surgery and + // seeking containers at the same time because we can end up requesting + // a local rewind on an element after binding it to a new container and then + // performing a rewind on that container as a whole without sampling in + // between. + // + // However, it should currently be impossible to get a rewind in the middle of + // a forwards seek since forwards seeks are detected and processed within the + // same (re)sample. + if (mSeekState == SEEK_NOT_SEEKING) { + mSeekState = mElementState == STATE_ACTIVE ? + SEEK_BACKWARD_FROM_ACTIVE : + SEEK_BACKWARD_FROM_INACTIVE; + } + MOZ_ASSERT(mSeekState == SEEK_BACKWARD_FROM_INACTIVE || + mSeekState == SEEK_BACKWARD_FROM_ACTIVE, + "Rewind in the middle of a forwards seek?"); + + ClearTimingState(RemoveNonDynamic); + RebuildTimingState(RemoveNonDynamic); + + MOZ_ASSERT(!mCurrentInterval, + "Current interval is set at end of rewind"); +} + +namespace +{ + bool + RemoveAll(nsSMILInstanceTime* aInstanceTime) + { + return true; + } +} // namespace + +bool +nsSMILTimedElement::SetIsDisabled(bool aIsDisabled) +{ + if (mIsDisabled == aIsDisabled) + return false; + + if (aIsDisabled) { + mIsDisabled = true; + ClearTimingState(RemoveAll); + } else { + RebuildTimingState(RemoveAll); + mIsDisabled = false; + } + return true; +} + +namespace +{ + bool + RemoveNonDOM(nsSMILInstanceTime* aInstanceTime) + { + return !aInstanceTime->FromDOM() && !aInstanceTime->ShouldPreserve(); + } +} // namespace + +bool +nsSMILTimedElement::SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, + Element* aContextNode, + nsresult* aParseResult) +{ + bool foundMatch = true; + nsresult parseResult = NS_OK; + + if (aAttribute == nsGkAtoms::begin) { + parseResult = SetBeginSpec(aValue, aContextNode, RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::dur) { + parseResult = SetSimpleDuration(aValue); + } else if (aAttribute == nsGkAtoms::end) { + parseResult = SetEndSpec(aValue, aContextNode, RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::fill) { + parseResult = SetFillMode(aValue); + } else if (aAttribute == nsGkAtoms::max) { + parseResult = SetMax(aValue); + } else if (aAttribute == nsGkAtoms::min) { + parseResult = SetMin(aValue); + } else if (aAttribute == nsGkAtoms::repeatCount) { + parseResult = SetRepeatCount(aValue); + } else if (aAttribute == nsGkAtoms::repeatDur) { + parseResult = SetRepeatDur(aValue); + } else if (aAttribute == nsGkAtoms::restart) { + parseResult = SetRestart(aValue); + } else { + foundMatch = false; + } + + if (foundMatch) { + aResult.SetTo(aValue); + if (aParseResult) { + *aParseResult = parseResult; + } + } + + return foundMatch; +} + +bool +nsSMILTimedElement::UnsetAttr(nsIAtom* aAttribute) +{ + bool foundMatch = true; + + if (aAttribute == nsGkAtoms::begin) { + UnsetBeginSpec(RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::dur) { + UnsetSimpleDuration(); + } else if (aAttribute == nsGkAtoms::end) { + UnsetEndSpec(RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::fill) { + UnsetFillMode(); + } else if (aAttribute == nsGkAtoms::max) { + UnsetMax(); + } else if (aAttribute == nsGkAtoms::min) { + UnsetMin(); + } else if (aAttribute == nsGkAtoms::repeatCount) { + UnsetRepeatCount(); + } else if (aAttribute == nsGkAtoms::repeatDur) { + UnsetRepeatDur(); + } else if (aAttribute == nsGkAtoms::restart) { + UnsetRestart(); + } else { + foundMatch = false; + } + + return foundMatch; +} + +//---------------------------------------------------------------------- +// Setters and unsetters + +nsresult +nsSMILTimedElement::SetBeginSpec(const nsAString& aBeginSpec, + Element* aContextNode, + RemovalTestFunction aRemove) +{ + return SetBeginOrEndSpec(aBeginSpec, aContextNode, true /*isBegin*/, + aRemove); +} + +void +nsSMILTimedElement::UnsetBeginSpec(RemovalTestFunction aRemove) +{ + ClearSpecs(mBeginSpecs, mBeginInstances, aRemove); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetEndSpec(const nsAString& aEndSpec, + Element* aContextNode, + RemovalTestFunction aRemove) +{ + return SetBeginOrEndSpec(aEndSpec, aContextNode, false /*!isBegin*/, + aRemove); +} + +void +nsSMILTimedElement::UnsetEndSpec(RemovalTestFunction aRemove) +{ + ClearSpecs(mEndSpecs, mEndInstances, aRemove); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetSimpleDuration(const nsAString& aDurSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + const nsAString& dur = nsSMILParserUtils::TrimWhitespace(aDurSpec); + + // SVG-specific: "For SVG's animation elements, if "media" is specified, the + // attribute will be ignored." (SVG 1.1, section 19.2.6) + if (dur.EqualsLiteral("media") || dur.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!nsSMILParserUtils::ParseClockValue(dur, &duration) || + duration.GetMillis() == 0L) { + mSimpleDur.SetIndefinite(); + return NS_ERROR_FAILURE; + } + } + // mSimpleDur should never be unresolved. ParseClockValue will either set + // duration to resolved or will return false. + MOZ_ASSERT(duration.IsResolved(), + "Setting unresolved simple duration"); + + mSimpleDur = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetSimpleDuration() +{ + mSimpleDur.SetIndefinite(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetMin(const nsAString& aMinSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + const nsAString& min = nsSMILParserUtils::TrimWhitespace(aMinSpec); + + if (min.EqualsLiteral("media")) { + duration.SetMillis(0L); + } else { + if (!nsSMILParserUtils::ParseClockValue(min, &duration)) { + mMin.SetMillis(0L); + return NS_ERROR_FAILURE; + } + } + + MOZ_ASSERT(duration.GetMillis() >= 0L, "Invalid duration"); + + mMin = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetMin() +{ + mMin.SetMillis(0L); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetMax(const nsAString& aMaxSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + const nsAString& max = nsSMILParserUtils::TrimWhitespace(aMaxSpec); + + if (max.EqualsLiteral("media") || max.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!nsSMILParserUtils::ParseClockValue(max, &duration) || + duration.GetMillis() == 0L) { + mMax.SetIndefinite(); + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(duration.GetMillis() > 0L, "Invalid duration"); + } + + mMax = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetMax() +{ + mMax.SetIndefinite(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetRestart(const nsAString& aRestartSpec) +{ + nsAttrValue temp; + bool parseResult + = temp.ParseEnumValue(aRestartSpec, sRestartModeTable, true); + mRestartMode = parseResult + ? nsSMILRestartMode(temp.GetEnumValue()) + : RESTART_ALWAYS; + UpdateCurrentInterval(); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILTimedElement::UnsetRestart() +{ + mRestartMode = RESTART_ALWAYS; + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetRepeatCount(const nsAString& aRepeatCountSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILRepeatCount newRepeatCount; + + if (nsSMILParserUtils::ParseRepeatCount(aRepeatCountSpec, newRepeatCount)) { + mRepeatCount = newRepeatCount; + return NS_OK; + } + mRepeatCount.Unset(); + return NS_ERROR_FAILURE; +} + +void +nsSMILTimedElement::UnsetRepeatCount() +{ + mRepeatCount.Unset(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetRepeatDur(const nsAString& aRepeatDurSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + + const nsAString& repeatDur = + nsSMILParserUtils::TrimWhitespace(aRepeatDurSpec); + + if (repeatDur.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!nsSMILParserUtils::ParseClockValue(repeatDur, &duration)) { + mRepeatDur.SetUnresolved(); + return NS_ERROR_FAILURE; + } + } + + mRepeatDur = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetRepeatDur() +{ + mRepeatDur.SetUnresolved(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetFillMode(const nsAString& aFillModeSpec) +{ + uint16_t previousFillMode = mFillMode; + + nsAttrValue temp; + bool parseResult = + temp.ParseEnumValue(aFillModeSpec, sFillModeTable, true); + mFillMode = parseResult + ? nsSMILFillMode(temp.GetEnumValue()) + : FILL_REMOVE; + + // Update fill mode of client + if (mFillMode != previousFillMode && HasClientInFillRange()) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + SampleFillValue(); + } + + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILTimedElement::UnsetFillMode() +{ + uint16_t previousFillMode = mFillMode; + mFillMode = FILL_REMOVE; + if (previousFillMode == FILL_FREEZE && HasClientInFillRange()) { + mClient->Inactivate(false); + } +} + +void +nsSMILTimedElement::AddDependent(nsSMILTimeValueSpec& aDependent) +{ + // There's probably no harm in attempting to register a dependent + // nsSMILTimeValueSpec twice, but we're not expecting it to happen. + MOZ_ASSERT(!mTimeDependents.GetEntry(&aDependent), + "nsSMILTimeValueSpec is already registered as a dependency"); + mTimeDependents.PutEntry(&aDependent); + + // Add current interval. We could add historical intervals too but that would + // cause unpredictable results since some intervals may have been filtered. + // SMIL doesn't say what to do here so for simplicity and consistency we + // simply add the current interval if there is one. + // + // It's not necessary to call SyncPauseTime since we're dealing with + // historical instance times not newly added ones. + if (mCurrentInterval) { + aDependent.HandleNewInterval(*mCurrentInterval, GetTimeContainer()); + } +} + +void +nsSMILTimedElement::RemoveDependent(nsSMILTimeValueSpec& aDependent) +{ + mTimeDependents.RemoveEntry(&aDependent); +} + +bool +nsSMILTimedElement::IsTimeDependent(const nsSMILTimedElement& aOther) const +{ + const nsSMILInstanceTime* thisBegin = GetEffectiveBeginInstance(); + const nsSMILInstanceTime* otherBegin = aOther.GetEffectiveBeginInstance(); + + if (!thisBegin || !otherBegin) + return false; + + return thisBegin->IsDependentOn(*otherBegin); +} + +void +nsSMILTimedElement::BindToTree(nsIContent* aContextNode) +{ + // Reset previously registered milestone since we may be registering with + // a different time container now. + mPrevRegisteredMilestone = sMaxMilestone; + + // If we were already active then clear all our timing information and start + // afresh + if (mElementState != STATE_STARTUP) { + mSeekState = SEEK_NOT_SEEKING; + Rewind(); + } + + // Scope updateBatcher to last only for the ResolveReferences calls: + { + AutoIntervalUpdateBatcher updateBatcher(*this); + + // Resolve references to other parts of the tree + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + mBeginSpecs[i]->ResolveReferences(aContextNode); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + mEndSpecs[j]->ResolveReferences(aContextNode); + } + } + + RegisterMilestone(); +} + +void +nsSMILTimedElement::HandleTargetElementChange(Element* aNewTarget) +{ + AutoIntervalUpdateBatcher updateBatcher(*this); + + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + mBeginSpecs[i]->HandleTargetElementChange(aNewTarget); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + mEndSpecs[j]->HandleTargetElementChange(aNewTarget); + } +} + +void +nsSMILTimedElement::Traverse(nsCycleCollectionTraversalCallback* aCallback) +{ + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsSMILTimeValueSpec* beginSpec = mBeginSpecs[i]; + MOZ_ASSERT(beginSpec, + "null nsSMILTimeValueSpec in list of begin specs"); + beginSpec->Traverse(aCallback); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + nsSMILTimeValueSpec* endSpec = mEndSpecs[j]; + MOZ_ASSERT(endSpec, "null nsSMILTimeValueSpec in list of end specs"); + endSpec->Traverse(aCallback); + } +} + +void +nsSMILTimedElement::Unlink() +{ + AutoIntervalUpdateBatcher updateBatcher(*this); + + // Remove dependencies on other elements + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsSMILTimeValueSpec* beginSpec = mBeginSpecs[i]; + MOZ_ASSERT(beginSpec, + "null nsSMILTimeValueSpec in list of begin specs"); + beginSpec->Unlink(); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + nsSMILTimeValueSpec* endSpec = mEndSpecs[j]; + MOZ_ASSERT(endSpec, "null nsSMILTimeValueSpec in list of end specs"); + endSpec->Unlink(); + } + + ClearIntervals(); + + // Make sure we don't notify other elements of new intervals + mTimeDependents.Clear(); +} + +//---------------------------------------------------------------------- +// Implementation helpers + +nsresult +nsSMILTimedElement::SetBeginOrEndSpec(const nsAString& aSpec, + Element* aContextNode, + bool aIsBegin, + RemovalTestFunction aRemove) +{ + TimeValueSpecList& timeSpecsList = aIsBegin ? mBeginSpecs : mEndSpecs; + InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances; + + ClearSpecs(timeSpecsList, instances, aRemove); + + AutoIntervalUpdateBatcher updateBatcher(*this); + + nsCharSeparatedTokenizer tokenizer(aSpec, ';'); + if (!tokenizer.hasMoreTokens()) { // Empty list + return NS_ERROR_FAILURE; + } + + nsresult rv = NS_OK; + while (tokenizer.hasMoreTokens() && NS_SUCCEEDED(rv)) { + nsAutoPtr + spec(new nsSMILTimeValueSpec(*this, aIsBegin)); + rv = spec->SetSpec(tokenizer.nextToken(), aContextNode); + if (NS_SUCCEEDED(rv)) { + timeSpecsList.AppendElement(spec.forget()); + } + } + + if (NS_FAILED(rv)) { + ClearSpecs(timeSpecsList, instances, aRemove); + } + + return rv; +} + +namespace +{ + // Adaptor functor for RemoveInstanceTimes that allows us to use function + // pointers instead. + // Without this we'd have to either templatize ClearSpecs and all its callers + // or pass bool flags around to specify which removal function to use here. + class MOZ_STACK_CLASS RemoveByFunction + { + public: + explicit RemoveByFunction(nsSMILTimedElement::RemovalTestFunction aFunction) + : mFunction(aFunction) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + return mFunction(aInstanceTime); + } + + private: + nsSMILTimedElement::RemovalTestFunction mFunction; + }; +} // namespace + +void +nsSMILTimedElement::ClearSpecs(TimeValueSpecList& aSpecs, + InstanceTimeList& aInstances, + RemovalTestFunction aRemove) +{ + AutoIntervalUpdateBatcher updateBatcher(*this); + + for (uint32_t i = 0; i < aSpecs.Length(); ++i) { + aSpecs[i]->Unlink(); + } + aSpecs.Clear(); + + RemoveByFunction removeByFunction(aRemove); + RemoveInstanceTimes(aInstances, removeByFunction); +} + +void +nsSMILTimedElement::ClearIntervals() +{ + if (mElementState != STATE_STARTUP) { + mElementState = STATE_POSTACTIVE; + } + mCurrentRepeatIteration = 0; + ResetCurrentInterval(); + + // Remove old intervals + for (int32_t i = mOldIntervals.Length() - 1; i >= 0; --i) { + mOldIntervals[i]->Unlink(); + } + mOldIntervals.Clear(); +} + +bool +nsSMILTimedElement::ApplyEarlyEnd(const nsSMILTimeValue& aSampleTime) +{ + // This should only be called within DoSampleAt as a helper function + MOZ_ASSERT(mElementState == STATE_ACTIVE, + "Unexpected state to try to apply an early end"); + + bool updated = false; + + // Only apply an early end if we're not already ending. + if (mCurrentInterval->End()->Time() > aSampleTime) { + nsSMILInstanceTime* earlyEnd = CheckForEarlyEnd(aSampleTime); + if (earlyEnd) { + if (earlyEnd->IsDependent()) { + // Generate a new instance time for the early end since the + // existing instance time is part of some dependency chain that we + // don't want to participate in. + RefPtr newEarlyEnd = + new nsSMILInstanceTime(earlyEnd->Time()); + mCurrentInterval->SetEnd(*newEarlyEnd); + } else { + mCurrentInterval->SetEnd(*earlyEnd); + } + updated = true; + } + } + return updated; +} + +namespace +{ + class MOZ_STACK_CLASS RemoveReset + { + public: + explicit RemoveReset(const nsSMILInstanceTime* aCurrentIntervalBegin) + : mCurrentIntervalBegin(aCurrentIntervalBegin) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + // SMIL 3.0 section 5.4.3, 'Resetting element state': + // Any instance times associated with past Event-values, Repeat-values, + // Accesskey-values or added via DOM method calls are removed from the + // dependent begin and end instance times lists. In effect, all events + // and DOM methods calls in the past are cleared. This does not apply to + // an instance time that defines the begin of the current interval. + return aInstanceTime->IsDynamic() && + !aInstanceTime->ShouldPreserve() && + (!mCurrentIntervalBegin || aInstanceTime != mCurrentIntervalBegin); + } + + private: + const nsSMILInstanceTime* mCurrentIntervalBegin; + }; +} // namespace + +void +nsSMILTimedElement::Reset() +{ + RemoveReset resetBegin(mCurrentInterval ? mCurrentInterval->Begin() : nullptr); + RemoveInstanceTimes(mBeginInstances, resetBegin); + + RemoveReset resetEnd(nullptr); + RemoveInstanceTimes(mEndInstances, resetEnd); +} + +void +nsSMILTimedElement::ClearTimingState(RemovalTestFunction aRemove) +{ + mElementState = STATE_STARTUP; + ClearIntervals(); + + UnsetBeginSpec(aRemove); + UnsetEndSpec(aRemove); + + if (mClient) { + mClient->Inactivate(false); + } +} + +void +nsSMILTimedElement::RebuildTimingState(RemovalTestFunction aRemove) +{ + MOZ_ASSERT(mAnimationElement, + "Attempting to enable a timed element not attached to an " + "animation element"); + MOZ_ASSERT(mElementState == STATE_STARTUP, + "Rebuilding timing state from non-startup state"); + + if (mAnimationElement->HasAnimAttr(nsGkAtoms::begin)) { + nsAutoString attValue; + mAnimationElement->GetAnimAttr(nsGkAtoms::begin, attValue); + SetBeginSpec(attValue, mAnimationElement, aRemove); + } + + if (mAnimationElement->HasAnimAttr(nsGkAtoms::end)) { + nsAutoString attValue; + mAnimationElement->GetAnimAttr(nsGkAtoms::end, attValue); + SetEndSpec(attValue, mAnimationElement, aRemove); + } + + mPrevRegisteredMilestone = sMaxMilestone; + RegisterMilestone(); +} + +void +nsSMILTimedElement::DoPostSeek() +{ + // Finish backwards seek + if (mSeekState == SEEK_BACKWARD_FROM_INACTIVE || + mSeekState == SEEK_BACKWARD_FROM_ACTIVE) { + // Previously some dynamic instance times may have been marked to be + // preserved because they were endpoints of an historic interval (which may + // or may not have been filtered). Now that we've finished a seek we should + // clear that flag for those instance times whose intervals are no longer + // historic. + UnpreserveInstanceTimes(mBeginInstances); + UnpreserveInstanceTimes(mEndInstances); + + // Now that the times have been unmarked perform a reset. This might seem + // counter-intuitive when we're only doing a seek within an interval but + // SMIL seems to require this. SMIL 3.0, 'Hyperlinks and timing': + // Resolved end times associated with events, Repeat-values, + // Accesskey-values or added via DOM method calls are cleared when seeking + // to time earlier than the resolved end time. + Reset(); + UpdateCurrentInterval(); + } + + switch (mSeekState) + { + case SEEK_FORWARD_FROM_ACTIVE: + case SEEK_BACKWARD_FROM_ACTIVE: + if (mElementState != STATE_ACTIVE) { + FireTimeEventAsync(eSMILEndEvent, 0); + } + break; + + case SEEK_FORWARD_FROM_INACTIVE: + case SEEK_BACKWARD_FROM_INACTIVE: + if (mElementState == STATE_ACTIVE) { + FireTimeEventAsync(eSMILBeginEvent, 0); + } + break; + + case SEEK_NOT_SEEKING: + /* Do nothing */ + break; + } + + mSeekState = SEEK_NOT_SEEKING; +} + +void +nsSMILTimedElement::UnpreserveInstanceTimes(InstanceTimeList& aList) +{ + const nsSMILInterval* prevInterval = GetPreviousInterval(); + const nsSMILInstanceTime* cutoff = mCurrentInterval ? + mCurrentInterval->Begin() : + prevInterval ? prevInterval->Begin() : nullptr; + uint32_t count = aList.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsSMILInstanceTime* instance = aList[i].get(); + if (!cutoff || cutoff->Time().CompareTo(instance->Time()) < 0) { + instance->UnmarkShouldPreserve(); + } + } +} + +void +nsSMILTimedElement::FilterHistory() +{ + // We should filter the intervals first, since instance times still used in an + // interval won't be filtered. + FilterIntervals(); + FilterInstanceTimes(mBeginInstances); + FilterInstanceTimes(mEndInstances); +} + +void +nsSMILTimedElement::FilterIntervals() +{ + // We can filter old intervals that: + // + // a) are not the previous interval; AND + // b) are not in the middle of a dependency chain; AND + // c) are not the first interval + // + // Condition (a) is necessary since the previous interval is used for applying + // fill effects and updating the current interval. + // + // Condition (b) is necessary since even if this interval itself is not + // active, it may be part of a dependency chain that includes active + // intervals. Such chains are used to establish priorities within the + // animation sandwich. + // + // Condition (c) is necessary to support hyperlinks that target animations + // since in some cases the defined behavior is to seek the document back to + // the first resolved begin time. Presumably the intention here is not + // actually to use the first resolved begin time, the + // _the_first_resolved_begin_time_that_produced_an_interval. That is, + // if we have begin="-5s; -3s; 1s; 3s" with a duration on 1s, we should seek + // to 1s. The spec doesn't say this but I'm pretty sure that is the intention. + // It seems negative times were simply not considered. + // + // Although the above conditions allow us to safely filter intervals for most + // scenarios they do not cover all cases and there will still be scenarios + // that generate intervals indefinitely. In such a case we simply set + // a maximum number of intervals and drop any intervals beyond that threshold. + + uint32_t threshold = mOldIntervals.Length() > sMaxNumIntervals ? + mOldIntervals.Length() - sMaxNumIntervals : + 0; + IntervalList filteredList; + for (uint32_t i = 0; i < mOldIntervals.Length(); ++i) + { + nsSMILInterval* interval = mOldIntervals[i].get(); + if (i != 0 && /*skip first interval*/ + i + 1 < mOldIntervals.Length() && /*skip previous interval*/ + (i < threshold || !interval->IsDependencyChainLink())) { + interval->Unlink(true /*filtered, not deleted*/); + } else { + filteredList.AppendElement(mOldIntervals[i].forget()); + } + } + mOldIntervals.Clear(); + mOldIntervals.SwapElements(filteredList); +} + +namespace +{ + class MOZ_STACK_CLASS RemoveFiltered + { + public: + explicit RemoveFiltered(nsSMILTimeValue aCutoff) : mCutoff(aCutoff) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + // We can filter instance times that: + // a) Precede the end point of the previous interval; AND + // b) Are NOT syncbase times that might be updated to a time after the end + // point of the previous interval; AND + // c) Are NOT fixed end points in any remaining interval. + return aInstanceTime->Time() < mCutoff && + aInstanceTime->IsFixedTime() && + !aInstanceTime->ShouldPreserve(); + } + + private: + nsSMILTimeValue mCutoff; + }; + + class MOZ_STACK_CLASS RemoveBelowThreshold + { + public: + RemoveBelowThreshold(uint32_t aThreshold, + nsTArray& aTimesToKeep) + : mThreshold(aThreshold), + mTimesToKeep(aTimesToKeep) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t aIndex) + { + return aIndex < mThreshold && !mTimesToKeep.Contains(aInstanceTime); + } + + private: + uint32_t mThreshold; + nsTArray& mTimesToKeep; + }; +} // namespace + +void +nsSMILTimedElement::FilterInstanceTimes(InstanceTimeList& aList) +{ + if (GetPreviousInterval()) { + RemoveFiltered removeFiltered(GetPreviousInterval()->End()->Time()); + RemoveInstanceTimes(aList, removeFiltered); + } + + // As with intervals it is possible to create a document that, even despite + // our most aggressive filtering, will generate instance times indefinitely + // (e.g. cyclic dependencies with TimeEvents---we can't filter such times as + // they're unpredictable due to the possibility of seeking the document which + // may prevent some events from being generated). Therefore we introduce + // a hard cutoff at which point we just drop the oldest instance times. + if (aList.Length() > sMaxNumInstanceTimes) { + uint32_t threshold = aList.Length() - sMaxNumInstanceTimes; + // There are a few instance times we should keep though, notably: + // - the current interval begin time, + // - the previous interval end time (see note in RemoveInstanceTimes) + // - the first interval begin time (see note in FilterIntervals) + nsTArray timesToKeep; + if (mCurrentInterval) { + timesToKeep.AppendElement(mCurrentInterval->Begin()); + } + const nsSMILInterval* prevInterval = GetPreviousInterval(); + if (prevInterval) { + timesToKeep.AppendElement(prevInterval->End()); + } + if (!mOldIntervals.IsEmpty()) { + timesToKeep.AppendElement(mOldIntervals[0]->Begin()); + } + RemoveBelowThreshold removeBelowThreshold(threshold, timesToKeep); + RemoveInstanceTimes(aList, removeBelowThreshold); + } +} + +// +// This method is based on the pseudocode given in the SMILANIM spec. +// +// See: +// http://www.w3.org/TR/2001/REC-smil-animation-20010904/#Timing-BeginEnd-LC-Start +// +bool +nsSMILTimedElement::GetNextInterval(const nsSMILInterval* aPrevInterval, + const nsSMILInterval* aReplacedInterval, + const nsSMILInstanceTime* aFixedBeginTime, + nsSMILInterval& aResult) const +{ + MOZ_ASSERT(!aFixedBeginTime || aFixedBeginTime->Time().IsDefinite(), + "Unresolved or indefinite begin time given for interval start"); + static const nsSMILTimeValue zeroTime(0L); + + if (mRestartMode == RESTART_NEVER && aPrevInterval) + return false; + + // Calc starting point + nsSMILTimeValue beginAfter; + bool prevIntervalWasZeroDur = false; + if (aPrevInterval) { + beginAfter = aPrevInterval->End()->Time(); + prevIntervalWasZeroDur + = aPrevInterval->End()->Time() == aPrevInterval->Begin()->Time(); + } else { + beginAfter.SetMillis(INT64_MIN); + } + + RefPtr tempBegin; + RefPtr tempEnd; + + while (true) { + // Calculate begin time + if (aFixedBeginTime) { + if (aFixedBeginTime->Time() < beginAfter) { + return false; + } + // our ref-counting is not const-correct + tempBegin = const_cast(aFixedBeginTime); + } else if ((!mAnimationElement || + !mAnimationElement->HasAnimAttr(nsGkAtoms::begin)) && + beginAfter <= zeroTime) { + tempBegin = new nsSMILInstanceTime(nsSMILTimeValue(0)); + } else { + int32_t beginPos = 0; + do { + tempBegin = + GetNextGreaterOrEqual(mBeginInstances, beginAfter, beginPos); + if (!tempBegin || !tempBegin->Time().IsDefinite()) { + return false; + } + // If we're updating the current interval then skip any begin time that is + // dependent on the current interval's begin time. e.g. + // GetBaseTime() == aReplacedInterval->Begin()); + } + MOZ_ASSERT(tempBegin && tempBegin->Time().IsDefinite() && + tempBegin->Time() >= beginAfter, + "Got a bad begin time while fetching next interval"); + + // Calculate end time + { + int32_t endPos = 0; + do { + tempEnd = + GetNextGreaterOrEqual(mEndInstances, tempBegin->Time(), endPos); + + // SMIL doesn't allow for coincident zero-duration intervals, so if the + // previous interval was zero-duration, and tempEnd is going to give us + // another zero duration interval, then look for another end to use + // instead. + if (tempEnd && prevIntervalWasZeroDur && + tempEnd->Time() == beginAfter) { + tempEnd = GetNextGreater(mEndInstances, tempBegin->Time(), endPos); + } + // As above with begin times, avoid creating self-referential loops + // between instance times by checking that the newly found end instance + // time is not already dependent on the end of the current interval. + } while (tempEnd && aReplacedInterval && + tempEnd->GetBaseTime() == aReplacedInterval->End()); + + if (!tempEnd) { + // If all the ends are before the beginning we have a bad interval + // UNLESS: + // a) We never had any end attribute to begin with (the SMIL pseudocode + // places this condition earlier in the flow but that fails to allow + // for DOM calls when no "indefinite" condition is given), OR + // b) We never had any end instance times to begin with, OR + // c) We have end events which leave the interval open-ended. + bool openEndedIntervalOk = mEndSpecs.IsEmpty() || + mEndInstances.IsEmpty() || + EndHasEventConditions(); + + // The above conditions correspond with the SMIL pseudocode but SMIL + // doesn't address self-dependent instance times which we choose to + // ignore. + // + // Therefore we add a qualification of (b) above that even if + // there are end instance times but they all depend on the end of the + // current interval we should act as if they didn't exist and allow the + // open-ended interval. + // + // In the following condition we don't use |= because it doesn't provide + // short-circuit behavior. + openEndedIntervalOk = openEndedIntervalOk || + (aReplacedInterval && + AreEndTimesDependentOn(aReplacedInterval->End())); + + if (!openEndedIntervalOk) { + return false; // Bad interval + } + } + + nsSMILTimeValue intervalEnd = tempEnd + ? tempEnd->Time() : nsSMILTimeValue(); + nsSMILTimeValue activeEnd = CalcActiveEnd(tempBegin->Time(), intervalEnd); + + if (!tempEnd || intervalEnd != activeEnd) { + tempEnd = new nsSMILInstanceTime(activeEnd); + } + } + MOZ_ASSERT(tempEnd, "Failed to get end point for next interval"); + + // When we choose the interval endpoints, we don't allow coincident + // zero-duration intervals, so if we arrive here and we have a zero-duration + // interval starting at the same point as a previous zero-duration interval, + // then it must be because we've applied constraints to the active duration. + // In that case, we will potentially run into an infinite loop, so we break + // it by searching for the next interval that starts AFTER our current + // zero-duration interval. + if (prevIntervalWasZeroDur && tempEnd->Time() == beginAfter) { + if (prevIntervalWasZeroDur) { + beginAfter.SetMillis(tempBegin->Time().GetMillis() + 1); + prevIntervalWasZeroDur = false; + continue; + } + } + prevIntervalWasZeroDur = tempBegin->Time() == tempEnd->Time(); + + // Check for valid interval + if (tempEnd->Time() > zeroTime || + (tempBegin->Time() == zeroTime && tempEnd->Time() == zeroTime)) { + aResult.Set(*tempBegin, *tempEnd); + return true; + } + + if (mRestartMode == RESTART_NEVER) { + // tempEnd <= 0 so we're going to loop which effectively means restarting + return false; + } + + beginAfter = tempEnd->Time(); + } + NS_NOTREACHED("Hmm... we really shouldn't be here"); + + return false; +} + +nsSMILInstanceTime* +nsSMILTimedElement::GetNextGreater(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const +{ + nsSMILInstanceTime* result = nullptr; + while ((result = GetNextGreaterOrEqual(aList, aBase, aPosition)) && + result->Time() == aBase) { } + return result; +} + +nsSMILInstanceTime* +nsSMILTimedElement::GetNextGreaterOrEqual(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const +{ + nsSMILInstanceTime* result = nullptr; + int32_t count = aList.Length(); + + for (; aPosition < count && !result; ++aPosition) { + nsSMILInstanceTime* val = aList[aPosition].get(); + MOZ_ASSERT(val, "NULL instance time in list"); + if (val->Time() >= aBase) { + result = val; + } + } + + return result; +} + +/** + * @see SMILANIM 3.3.4 + */ +nsSMILTimeValue +nsSMILTimedElement::CalcActiveEnd(const nsSMILTimeValue& aBegin, + const nsSMILTimeValue& aEnd) const +{ + nsSMILTimeValue result; + + MOZ_ASSERT(mSimpleDur.IsResolved(), + "Unresolved simple duration in CalcActiveEnd"); + MOZ_ASSERT(aBegin.IsDefinite(), + "Indefinite or unresolved begin time in CalcActiveEnd"); + + result = GetRepeatDuration(); + + if (aEnd.IsDefinite()) { + nsSMILTime activeDur = aEnd.GetMillis() - aBegin.GetMillis(); + + if (result.IsDefinite()) { + result.SetMillis(std::min(result.GetMillis(), activeDur)); + } else { + result.SetMillis(activeDur); + } + } + + result = ApplyMinAndMax(result); + + if (result.IsDefinite()) { + nsSMILTime activeEnd = result.GetMillis() + aBegin.GetMillis(); + result.SetMillis(activeEnd); + } + + return result; +} + +nsSMILTimeValue +nsSMILTimedElement::GetRepeatDuration() const +{ + nsSMILTimeValue multipliedDuration; + if (mRepeatCount.IsDefinite() && mSimpleDur.IsDefinite()) { + multipliedDuration.SetMillis( + nsSMILTime(mRepeatCount * double(mSimpleDur.GetMillis()))); + } else { + multipliedDuration.SetIndefinite(); + } + + nsSMILTimeValue repeatDuration; + + if (mRepeatDur.IsResolved()) { + repeatDuration = std::min(multipliedDuration, mRepeatDur); + } else if (mRepeatCount.IsSet()) { + repeatDuration = multipliedDuration; + } else { + repeatDuration = mSimpleDur; + } + + return repeatDuration; +} + +nsSMILTimeValue +nsSMILTimedElement::ApplyMinAndMax(const nsSMILTimeValue& aDuration) const +{ + if (!aDuration.IsResolved()) { + return aDuration; + } + + if (mMax < mMin) { + return aDuration; + } + + nsSMILTimeValue result; + + if (aDuration > mMax) { + result = mMax; + } else if (aDuration < mMin) { + result = mMin; + } else { + result = aDuration; + } + + return result; +} + +nsSMILTime +nsSMILTimedElement::ActiveTimeToSimpleTime(nsSMILTime aActiveTime, + uint32_t& aRepeatIteration) +{ + nsSMILTime result; + + MOZ_ASSERT(mSimpleDur.IsResolved(), + "Unresolved simple duration in ActiveTimeToSimpleTime"); + MOZ_ASSERT(aActiveTime >= 0, "Expecting non-negative active time"); + // Note that a negative aActiveTime will give us a negative value for + // aRepeatIteration, which is bad because aRepeatIteration is unsigned + + if (mSimpleDur.IsIndefinite() || mSimpleDur.GetMillis() == 0L) { + aRepeatIteration = 0; + result = aActiveTime; + } else { + result = aActiveTime % mSimpleDur.GetMillis(); + aRepeatIteration = (uint32_t)(aActiveTime / mSimpleDur.GetMillis()); + } + + return result; +} + +// +// Although in many cases it would be possible to check for an early end and +// adjust the current interval well in advance the SMIL Animation spec seems to +// indicate that we should only apply an early end at the latest possible +// moment. In particular, this paragraph from section 3.6.8: +// +// 'If restart is set to "always", then the current interval will end early if +// there is an instance time in the begin list that is before (i.e. earlier +// than) the defined end for the current interval. Ending in this manner will +// also send a changed time notice to all time dependents for the current +// interval end.' +// +nsSMILInstanceTime* +nsSMILTimedElement::CheckForEarlyEnd( + const nsSMILTimeValue& aContainerTime) const +{ + MOZ_ASSERT(mCurrentInterval, + "Checking for an early end but the current interval is not set"); + if (mRestartMode != RESTART_ALWAYS) + return nullptr; + + int32_t position = 0; + nsSMILInstanceTime* nextBegin = + GetNextGreater(mBeginInstances, mCurrentInterval->Begin()->Time(), + position); + + if (nextBegin && + nextBegin->Time() > mCurrentInterval->Begin()->Time() && + nextBegin->Time() < mCurrentInterval->End()->Time() && + nextBegin->Time() <= aContainerTime) { + return nextBegin; + } + + return nullptr; +} + +void +nsSMILTimedElement::UpdateCurrentInterval(bool aForceChangeNotice) +{ + // Check if updates are currently blocked (batched) + if (mDeferIntervalUpdates) { + mDoDeferredUpdate = true; + return; + } + + // We adopt the convention of not resolving intervals until the first + // sample. Otherwise, every time each attribute is set we'll re-resolve the + // current interval and notify all our time dependents of the change. + // + // The disadvantage of deferring resolving the interval is that DOM calls to + // to getStartTime will throw an INVALID_STATE_ERR exception until the + // document timeline begins since the start time has not yet been resolved. + if (mElementState == STATE_STARTUP) + return; + + // Although SMIL gives rules for detecting cycles in change notifications, + // some configurations can lead to create-delete-create-delete-etc. cycles + // which SMIL does not consider. + // + // In order to provide consistent behavior in such cases, we detect two + // deletes in a row and then refuse to create any further intervals. That is, + // we say the configuration is invalid. + if (mDeleteCount > 1) { + // When we update the delete count we also set the state to post active, so + // if we're not post active here then something other than + // UpdateCurrentInterval has updated the element state in between and all + // bets are off. + MOZ_ASSERT(mElementState == STATE_POSTACTIVE, + "Expected to be in post-active state after performing double " + "delete"); + return; + } + + // Check that we aren't stuck in infinite recursion updating some syncbase + // dependencies. Generally such situations should be detected in advance and + // the chain broken in a sensible and predictable manner, so if we're hitting + // this assertion we need to work out how to detect the case that's causing + // it. In release builds, just bail out before we overflow the stack. + AutoRestore depthRestorer(mUpdateIntervalRecursionDepth); + if (++mUpdateIntervalRecursionDepth > sMaxUpdateIntervalRecursionDepth) { + MOZ_ASSERT(false, + "Update current interval recursion depth exceeded threshold"); + return; + } + + // If the interval is active the begin time is fixed. + const nsSMILInstanceTime* beginTime = mElementState == STATE_ACTIVE + ? mCurrentInterval->Begin() + : nullptr; + nsSMILInterval updatedInterval; + if (GetNextInterval(GetPreviousInterval(), mCurrentInterval, + beginTime, updatedInterval)) { + + if (mElementState == STATE_POSTACTIVE) { + + MOZ_ASSERT(!mCurrentInterval, + "In postactive state but the interval has been set"); + mCurrentInterval = new nsSMILInterval(updatedInterval); + mElementState = STATE_WAITING; + NotifyNewInterval(); + + } else { + + bool beginChanged = false; + bool endChanged = false; + + if (mElementState != STATE_ACTIVE && + !updatedInterval.Begin()->SameTimeAndBase( + *mCurrentInterval->Begin())) { + mCurrentInterval->SetBegin(*updatedInterval.Begin()); + beginChanged = true; + } + + if (!updatedInterval.End()->SameTimeAndBase(*mCurrentInterval->End())) { + mCurrentInterval->SetEnd(*updatedInterval.End()); + endChanged = true; + } + + if (beginChanged || endChanged || aForceChangeNotice) { + NotifyChangedInterval(mCurrentInterval, beginChanged, endChanged); + } + } + + // There's a chance our next milestone has now changed, so update the time + // container + RegisterMilestone(); + } else { // GetNextInterval failed: Current interval is no longer valid + if (mElementState == STATE_ACTIVE) { + // The interval is active so we can't just delete it, instead trim it so + // that begin==end. + if (!mCurrentInterval->End()->SameTimeAndBase(*mCurrentInterval->Begin())) + { + mCurrentInterval->SetEnd(*mCurrentInterval->Begin()); + NotifyChangedInterval(mCurrentInterval, false, true); + } + // The transition to the postactive state will take place on the next + // sample (along with firing end events, clearing intervals etc.) + RegisterMilestone(); + } else if (mElementState == STATE_WAITING) { + AutoRestore deleteCountRestorer(mDeleteCount); + ++mDeleteCount; + mElementState = STATE_POSTACTIVE; + ResetCurrentInterval(); + } + } +} + +void +nsSMILTimedElement::SampleSimpleTime(nsSMILTime aActiveTime) +{ + if (mClient) { + uint32_t repeatIteration; + nsSMILTime simpleTime = + ActiveTimeToSimpleTime(aActiveTime, repeatIteration); + mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration); + } +} + +void +nsSMILTimedElement::SampleFillValue() +{ + if (mFillMode != FILL_FREEZE || !mClient) + return; + + nsSMILTime activeTime; + + if (mElementState == STATE_WAITING || mElementState == STATE_POSTACTIVE) { + const nsSMILInterval* prevInterval = GetPreviousInterval(); + MOZ_ASSERT(prevInterval, + "Attempting to sample fill value but there is no previous " + "interval"); + MOZ_ASSERT(prevInterval->End()->Time().IsDefinite() && + prevInterval->End()->IsFixedTime(), + "Attempting to sample fill value but the endpoint of the " + "previous interval is not resolved and fixed"); + + activeTime = prevInterval->End()->Time().GetMillis() - + prevInterval->Begin()->Time().GetMillis(); + + // If the interval's repeat duration was shorter than its active duration, + // use the end of the repeat duration to determine the frozen animation's + // state. + nsSMILTimeValue repeatDuration = GetRepeatDuration(); + if (repeatDuration.IsDefinite()) { + activeTime = std::min(repeatDuration.GetMillis(), activeTime); + } + } else { + MOZ_ASSERT(mElementState == STATE_ACTIVE, + "Attempting to sample fill value when we're in an unexpected state " + "(probably STATE_STARTUP)"); + + // If we are being asked to sample the fill value while active we *must* + // have a repeat duration shorter than the active duration so use that. + MOZ_ASSERT(GetRepeatDuration().IsDefinite(), + "Attempting to sample fill value of an active animation with " + "an indefinite repeat duration"); + activeTime = GetRepeatDuration().GetMillis(); + } + + uint32_t repeatIteration; + nsSMILTime simpleTime = + ActiveTimeToSimpleTime(activeTime, repeatIteration); + + if (simpleTime == 0L && repeatIteration) { + mClient->SampleLastValue(--repeatIteration); + } else { + mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration); + } +} + +nsresult +nsSMILTimedElement::AddInstanceTimeFromCurrentTime(nsSMILTime aCurrentTime, + double aOffsetSeconds, bool aIsBegin) +{ + double offset = aOffsetSeconds * PR_MSEC_PER_SEC; + + // Check we won't overflow the range of nsSMILTime + if (aCurrentTime + NS_round(offset) > INT64_MAX) + return NS_ERROR_ILLEGAL_VALUE; + + nsSMILTimeValue timeVal(aCurrentTime + int64_t(NS_round(offset))); + + RefPtr instanceTime = + new nsSMILInstanceTime(timeVal, nsSMILInstanceTime::SOURCE_DOM); + + AddInstanceTime(instanceTime, aIsBegin); + + return NS_OK; +} + +void +nsSMILTimedElement::RegisterMilestone() +{ + nsSMILTimeContainer* container = GetTimeContainer(); + if (!container) + return; + MOZ_ASSERT(mAnimationElement, + "Got a time container without an owning animation element"); + + nsSMILMilestone nextMilestone; + if (!GetNextMilestone(nextMilestone)) + return; + + // This method is called every time we might possibly have updated our + // current interval, but since nsSMILTimeContainer makes no attempt to filter + // out redundant milestones we do some rudimentary filtering here. It's not + // perfect, but unnecessary samples are fairly cheap. + if (nextMilestone >= mPrevRegisteredMilestone) + return; + + container->AddMilestone(nextMilestone, *mAnimationElement); + mPrevRegisteredMilestone = nextMilestone; +} + +bool +nsSMILTimedElement::GetNextMilestone(nsSMILMilestone& aNextMilestone) const +{ + // Return the next key moment in our lifetime. + // + // XXX It may be possible in future to optimise this so that we only register + // for milestones if: + // a) We have time dependents, or + // b) We are dependent on events or syncbase relationships, or + // c) There are registered listeners for our events + // + // Then for the simple case where everything uses offset values we could + // ignore milestones altogether. + // + // We'd need to be careful, however, that if one of those conditions became + // true in between samples that we registered our next milestone at that + // point. + + switch (mElementState) + { + case STATE_STARTUP: + // All elements register for an initial end sample at t=0 where we resolve + // our initial interval. + aNextMilestone.mIsEnd = true; // Initial sample should be an end sample + aNextMilestone.mTime = 0; + return true; + + case STATE_WAITING: + MOZ_ASSERT(mCurrentInterval, + "In waiting state but the current interval has not been set"); + aNextMilestone.mIsEnd = false; + aNextMilestone.mTime = mCurrentInterval->Begin()->Time().GetMillis(); + return true; + + case STATE_ACTIVE: + { + // Work out what comes next: the interval end or the next repeat iteration + nsSMILTimeValue nextRepeat; + if (mSeekState == SEEK_NOT_SEEKING && mSimpleDur.IsDefinite()) { + nsSMILTime nextRepeatActiveTime = + (mCurrentRepeatIteration + 1) * mSimpleDur.GetMillis(); + // Check that the repeat fits within the repeat duration + if (nsSMILTimeValue(nextRepeatActiveTime) < GetRepeatDuration()) { + nextRepeat.SetMillis(mCurrentInterval->Begin()->Time().GetMillis() + + nextRepeatActiveTime); + } + } + nsSMILTimeValue nextMilestone = + std::min(mCurrentInterval->End()->Time(), nextRepeat); + + // Check for an early end before that time + nsSMILInstanceTime* earlyEnd = CheckForEarlyEnd(nextMilestone); + if (earlyEnd) { + aNextMilestone.mIsEnd = true; + aNextMilestone.mTime = earlyEnd->Time().GetMillis(); + return true; + } + + // Apply the previously calculated milestone + if (nextMilestone.IsDefinite()) { + aNextMilestone.mIsEnd = nextMilestone != nextRepeat; + aNextMilestone.mTime = nextMilestone.GetMillis(); + return true; + } + + return false; + } + + case STATE_POSTACTIVE: + return false; + } + MOZ_CRASH("Invalid element state"); +} + +void +nsSMILTimedElement::NotifyNewInterval() +{ + MOZ_ASSERT(mCurrentInterval, + "Attempting to notify dependents of a new interval but the " + "interval is not set"); + + nsSMILTimeContainer* container = GetTimeContainer(); + if (container) { + container->SyncPauseTime(); + } + + for (auto iter = mTimeDependents.Iter(); !iter.Done(); iter.Next()) { + nsSMILInterval* interval = mCurrentInterval; + // It's possible that in notifying one new time dependent of a new interval + // that a chain reaction is triggered which results in the original + // interval disappearing. If that's the case we can skip sending further + // notifications. + if (!interval) { + break; + } + nsSMILTimeValueSpec* spec = iter.Get()->GetKey(); + spec->HandleNewInterval(*interval, container); + } +} + +void +nsSMILTimedElement::NotifyChangedInterval(nsSMILInterval* aInterval, + bool aBeginObjectChanged, + bool aEndObjectChanged) +{ + MOZ_ASSERT(aInterval, "Null interval for change notification"); + + nsSMILTimeContainer* container = GetTimeContainer(); + if (container) { + container->SyncPauseTime(); + } + + // Copy the instance times list since notifying the instance times can result + // in a chain reaction whereby our own interval gets deleted along with its + // instance times. + InstanceTimeList times; + aInterval->GetDependentTimes(times); + + for (uint32_t i = 0; i < times.Length(); ++i) { + times[i]->HandleChangedInterval(container, aBeginObjectChanged, + aEndObjectChanged); + } +} + +void +nsSMILTimedElement::FireTimeEventAsync(EventMessage aMsg, int32_t aDetail) +{ + if (!mAnimationElement) + return; + + nsCOMPtr event = + new AsyncTimeEventRunner(mAnimationElement, aMsg, aDetail); + NS_DispatchToMainThread(event); +} + +const nsSMILInstanceTime* +nsSMILTimedElement::GetEffectiveBeginInstance() const +{ + switch (mElementState) + { + case STATE_STARTUP: + return nullptr; + + case STATE_ACTIVE: + return mCurrentInterval->Begin(); + + case STATE_WAITING: + case STATE_POSTACTIVE: + { + const nsSMILInterval* prevInterval = GetPreviousInterval(); + return prevInterval ? prevInterval->Begin() : nullptr; + } + } + MOZ_CRASH("Invalid element state"); +} + +const nsSMILInterval* +nsSMILTimedElement::GetPreviousInterval() const +{ + return mOldIntervals.IsEmpty() + ? nullptr + : mOldIntervals[mOldIntervals.Length()-1].get(); +} + +bool +nsSMILTimedElement::HasClientInFillRange() const +{ + // Returns true if we have a client that is in the range where it will fill + return mClient && + ((mElementState != STATE_ACTIVE && HasPlayed()) || + (mElementState == STATE_ACTIVE && !mClient->IsActive())); +} + +bool +nsSMILTimedElement::EndHasEventConditions() const +{ + for (uint32_t i = 0; i < mEndSpecs.Length(); ++i) { + if (mEndSpecs[i]->IsEventBased()) + return true; + } + return false; +} + +bool +nsSMILTimedElement::AreEndTimesDependentOn( + const nsSMILInstanceTime* aBase) const +{ + if (mEndInstances.IsEmpty()) + return false; + + for (uint32_t i = 0; i < mEndInstances.Length(); ++i) { + if (mEndInstances[i]->GetBaseTime() != aBase) { + return false; + } + } + return true; +} + diff --git a/dom/smil/nsSMILTimedElement.h b/dom/smil/nsSMILTimedElement.h new file mode 100644 index 000000000..1831deeb0 --- /dev/null +++ b/dom/smil/nsSMILTimedElement.h @@ -0,0 +1,673 @@ +/* -*- 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/. */ + +#ifndef NS_SMILTIMEDELEMENT_H_ +#define NS_SMILTIMEDELEMENT_H_ + +#include "mozilla/EventForwards.h" +#include "mozilla/Move.h" +#include "nsSMILInterval.h" +#include "nsSMILInstanceTime.h" +#include "nsSMILMilestone.h" +#include "nsSMILTimeValueSpec.h" +#include "nsSMILRepeatCount.h" +#include "nsSMILTypes.h" +#include "nsTArray.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsAutoPtr.h" +#include "nsAttrValue.h" + +class nsSMILAnimationFunction; +class nsSMILTimeContainer; +class nsSMILTimeValue; +class nsIAtom; + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +//---------------------------------------------------------------------- +// nsSMILTimedElement + +class nsSMILTimedElement +{ +public: + nsSMILTimedElement(); + ~nsSMILTimedElement(); + + typedef mozilla::dom::Element Element; + + /* + * Sets the owning animation element which this class uses to convert between + * container times and to register timebase elements. + */ + void SetAnimationElement(mozilla::dom::SVGAnimationElement* aElement); + + /* + * Returns the time container with which this timed element is associated or + * nullptr if it is not associated with a time container. + */ + nsSMILTimeContainer* GetTimeContainer(); + + /* + * Returns the element targeted by the animation element. Needed for + * registering event listeners against the appropriate element. + */ + mozilla::dom::Element* GetTargetElement(); + + /** + * Methods for supporting the nsIDOMElementTimeControl interface. + */ + + /* + * Adds a new begin instance time at the current container time plus or minus + * the specified offset. + * + * @param aOffsetSeconds A real number specifying the number of seconds to add + * to the current container time. + * @return NS_OK if the operation succeeeded, or an error code otherwise. + */ + nsresult BeginElementAt(double aOffsetSeconds); + + /* + * Adds a new end instance time at the current container time plus or minus + * the specified offset. + * + * @param aOffsetSeconds A real number specifying the number of seconds to add + * to the current container time. + * @return NS_OK if the operation succeeeded, or an error code otherwise. + */ + nsresult EndElementAt(double aOffsetSeconds); + + /** + * Methods for supporting the nsSVGAnimationElement interface. + */ + + /** + * According to SVG 1.1 SE this returns + * + * the begin time, in seconds, for this animation element's current + * interval, if it exists, regardless of whether the interval has begun yet. + * + * @return the start time as defined above in milliseconds or an unresolved + * time if there is no current interval. + */ + nsSMILTimeValue GetStartTime() const; + + /** + * Returns the simple duration of this element. + * + * @return the simple duration in milliseconds or INDEFINITE. + */ + nsSMILTimeValue GetSimpleDuration() const + { + return mSimpleDur; + } + + /** + * Methods for supporting hyperlinking + */ + + /** + * Internal SMIL methods + */ + + /** + * Returns the time to seek the document to when this element is targetted by + * a hyperlink. + * + * The behavior is defined here: + * http://www.w3.org/TR/smil-animation/#HyperlinkSemantics + * + * It is very similar to GetStartTime() with the exception that when the + * element is not active, the begin time of the *first* interval is returned. + * + * @return the time to seek the documen to in milliseconds or an unresolved + * time if there is no resolved interval. + */ + nsSMILTimeValue GetHyperlinkTime() const; + + /** + * Adds an instance time object this element's list of instance times. + * These instance times are used when creating intervals. + * + * This method is typically called by an nsSMILTimeValueSpec. + * + * @param aInstanceTime The time to add, expressed in container time. + * @param aIsBegin true if the time to be added represents a begin + * time or false if it represents an end time. + */ + void AddInstanceTime(nsSMILInstanceTime* aInstanceTime, bool aIsBegin); + + /** + * Requests this element update the given instance time. + * + * This method is typically called by a child nsSMILTimeValueSpec. + * + * @param aInstanceTime The instance time to update. + * @param aUpdatedTime The time to update aInstanceTime with. + * @param aDependentTime The instance time upon which aInstanceTime should be + * based. + * @param aIsBegin true if the time to be updated represents a begin + * instance time or false if it represents an end + * instance time. + */ + void UpdateInstanceTime(nsSMILInstanceTime* aInstanceTime, + nsSMILTimeValue& aUpdatedTime, + bool aIsBegin); + + /** + * Removes an instance time object from this element's list of instance times. + * + * This method is typically called by a child nsSMILTimeValueSpec. + * + * @param aInstanceTime The instance time to remove. + * @param aIsBegin true if the time to be removed represents a begin + * time or false if it represents an end time. + */ + void RemoveInstanceTime(nsSMILInstanceTime* aInstanceTime, bool aIsBegin); + + /** + * Removes all the instance times associated with the given + * nsSMILTimeValueSpec object. Used when an ID assignment changes and hence + * all the previously associated instance times become invalid. + * + * @param aSpec The nsSMILTimeValueSpec object whose created + * nsSMILInstanceTime's should be removed. + * @param aIsBegin true if the times to be removed represent begin + * times or false if they are end times. + */ + void RemoveInstanceTimesForCreator(const nsSMILTimeValueSpec* aSpec, + bool aIsBegin); + + /** + * Sets the object that will be called by this timed element each time it is + * sampled. + * + * In Schmitz's model it is possible to associate several time clients with + * a timed element but for now we only allow one. + * + * @param aClient The time client to associate. Any previous time client + * will be disassociated and no longer sampled. Setting this + * to nullptr will simply disassociate the previous client, if + * any. + */ + void SetTimeClient(nsSMILAnimationFunction* aClient); + + /** + * Samples the object at the given container time. Timing intervals are + * updated and if this element is active at the given time the associated time + * client will be sampled with the appropriate simple time. + * + * @param aContainerTime The container time at which to sample. + */ + void SampleAt(nsSMILTime aContainerTime); + + /** + * Performs a special sample for the end of an interval. Such a sample should + * only advance the timed element (and any dependent elements) to the waiting + * or postactive state. It should not cause a transition to the active state. + * Transition to the active state is only performed on a regular SampleAt. + * + * This allows all interval ends at a given time to be processed first and + * hence the new interval can be established based on full information of the + * available instance times. + * + * @param aContainerTime The container time at which to sample. + */ + void SampleEndAt(nsSMILTime aContainerTime); + + /** + * Informs the timed element that its time container has changed time + * relative to document time. The timed element therefore needs to update its + * dependent elements (which may belong to a different time container) so they + * can re-resolve their times. + */ + void HandleContainerTimeChange(); + + /** + * Resets this timed element's accumulated times and intervals back to start + * up state. + * + * This is used for backwards seeking where rather than accumulating + * historical timing state and winding it back, we reset the element and seek + * forwards. + */ + void Rewind(); + + /** + * Marks this element as disabled or not. If the element is disabled, it + * will ignore any future samples and discard any accumulated timing state. + * + * This is used by SVG to "turn off" timed elements when the associated + * animation element has failing conditional processing tests. + * + * Returns true if the disabled state of the timed element was changed + * as a result of this call (i.e. it was not a redundant call). + */ + bool SetIsDisabled(bool aIsDisabled); + + /** + * Attempts to set an attribute on this timed element. + * + * @param aAttribute The name of the attribute to set. The namespace of this + * attribute is not specified as it is checked by the host + * element. Only attributes in the namespace defined for + * SMIL attributes in the host language are passed to the + * timed element. + * @param aValue The attribute value. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aContextNode The element to use for context when resolving + * references to other elements. + * @param[out] aParseResult The result of parsing the attribute. Will be set + * to NS_OK if parsing is successful. + * + * @return true if the given attribute is a timing attribute, false + * otherwise. + */ + bool SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, Element* aContextNode, + nsresult* aParseResult = nullptr); + + /** + * Attempts to unset an attribute on this timed element. + * + * @param aAttribute The name of the attribute to set. As with SetAttr the + * namespace of the attribute is not specified (see + * SetAttr). + * + * @return true if the given attribute is a timing attribute, false + * otherwise. + */ + bool UnsetAttr(nsIAtom* aAttribute); + + /** + * Adds a syncbase dependency to the list of dependents that will be notified + * when this timed element creates, deletes, or updates its current interval. + * + * @param aDependent The nsSMILTimeValueSpec object to notify. A raw pointer + * to this object will be stored. Therefore it is necessary + * for the object to be explicitly unregistered (with + * RemoveDependent) when it is destroyed. + */ + void AddDependent(nsSMILTimeValueSpec& aDependent); + + /** + * Removes a syncbase dependency from the list of dependents that are notified + * when the current interval is modified. + * + * @param aDependent The nsSMILTimeValueSpec object to unregister. + */ + void RemoveDependent(nsSMILTimeValueSpec& aDependent); + + /** + * Determines if this timed element is dependent on the given timed element's + * begin time for the interval currently in effect. Whilst the element is in + * the active state this is the current interval and in the postactive or + * waiting state this is the previous interval if one exists. In all other + * cases the element is not considered a time dependent of any other element. + * + * @param aOther The potential syncbase element. + * @return true if this timed element's begin time for the currently + * effective interval is directly or indirectly derived from aOther, false + * otherwise. + */ + bool IsTimeDependent(const nsSMILTimedElement& aOther) const; + + /** + * Called when the timed element has been bound to the document so that + * references from this timed element to other elements can be resolved. + * + * @param aContextNode The node which provides the necessary context for + * resolving references. This is typically the element in + * the host language that owns this timed element. Should + * not be null. + */ + void BindToTree(nsIContent* aContextNode); + + /** + * Called when the target of the animation has changed so that event + * registrations can be updated. + */ + void HandleTargetElementChange(mozilla::dom::Element* aNewTarget); + + /** + * Called when the timed element has been removed from a document so that + * references to other elements can be broken. + */ + void DissolveReferences() { Unlink(); } + + // Cycle collection + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + typedef bool (*RemovalTestFunction)(nsSMILInstanceTime* aInstance); + +protected: + // Typedefs + typedef nsTArray > TimeValueSpecList; + typedef nsTArray > InstanceTimeList; + typedef nsTArray > IntervalList; + typedef nsPtrHashKey TimeValueSpecPtrKey; + typedef nsTHashtable TimeValueSpecHashSet; + + // Helper classes + class InstanceTimeComparator { + public: + bool Equals(const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const; + bool LessThan(const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const; + }; + + // Templated helper functions + template + void RemoveInstanceTimes(InstanceTimeList& aArray, TestFunctor& aTest); + + // + // Implementation helpers + // + + nsresult SetBeginSpec(const nsAString& aBeginSpec, + Element* aContextNode, + RemovalTestFunction aRemove); + nsresult SetEndSpec(const nsAString& aEndSpec, + Element* aContextNode, + RemovalTestFunction aRemove); + nsresult SetSimpleDuration(const nsAString& aDurSpec); + nsresult SetMin(const nsAString& aMinSpec); + nsresult SetMax(const nsAString& aMaxSpec); + nsresult SetRestart(const nsAString& aRestartSpec); + nsresult SetRepeatCount(const nsAString& aRepeatCountSpec); + nsresult SetRepeatDur(const nsAString& aRepeatDurSpec); + nsresult SetFillMode(const nsAString& aFillModeSpec); + + void UnsetBeginSpec(RemovalTestFunction aRemove); + void UnsetEndSpec(RemovalTestFunction aRemove); + void UnsetSimpleDuration(); + void UnsetMin(); + void UnsetMax(); + void UnsetRestart(); + void UnsetRepeatCount(); + void UnsetRepeatDur(); + void UnsetFillMode(); + + nsresult SetBeginOrEndSpec(const nsAString& aSpec, + Element* aContextNode, + bool aIsBegin, + RemovalTestFunction aRemove); + void ClearSpecs(TimeValueSpecList& aSpecs, + InstanceTimeList& aInstances, + RemovalTestFunction aRemove); + void ClearIntervals(); + void DoSampleAt(nsSMILTime aContainerTime, bool aEndOnly); + + /** + * Helper function to check for an early end and, if necessary, update the + * current interval accordingly. + * + * See SMIL 3.0, section 5.4.5, Element life cycle, "Active Time - Playing an + * interval" for a description of ending early. + * + * @param aSampleTime The current sample time. Early ends should only be + * applied at the last possible moment (i.e. if they are at + * or before the current sample time) and only if the + * current interval is not already ending. + * @return true if the end time of the current interval was updated, + * false otherwise. + */ + bool ApplyEarlyEnd(const nsSMILTimeValue& aSampleTime); + + /** + * Clears certain state in response to the element restarting. + * + * This state is described in SMIL 3.0, section 5.4.3, Resetting element state + */ + void Reset(); + + /** + * Clears all accumulated timing state except for those instance times for + * which aRemove does not return true. + * + * Unlike the Reset method which only clears instance times, this clears the + * element's state, intervals (including current interval), and tells the + * client animation function to stop applying a result. In effect, it returns + * the element to its initial state but preserves any instance times excluded + * by the passed-in function. + */ + void ClearTimingState(RemovalTestFunction aRemove); + + /** + * Recreates timing state by re-applying begin/end attributes specified on + * the associated animation element. + * + * Note that this does not completely restore the information cleared by + * ClearTimingState since it leaves the element in the startup state. + * The element state will be updated on the next sample. + */ + void RebuildTimingState(RemovalTestFunction aRemove); + + /** + * Completes a seek operation by sending appropriate events and, in the case + * of a backwards seek, updating the state of timing information that was + * previously considered historical. + */ + void DoPostSeek(); + + /** + * Unmarks instance times that were previously preserved because they were + * considered important historical milestones but are no longer such because + * a backwards seek has been performed. + */ + void UnpreserveInstanceTimes(InstanceTimeList& aList); + + /** + * Helper function to iterate through this element's accumulated timing + * information (specifically old nsSMILIntervals and nsSMILTimeInstanceTimes) + * and discard items that are no longer needed or exceed some threshold of + * accumulated state. + */ + void FilterHistory(); + + // Helper functions for FilterHistory to clear old nsSMILIntervals and + // nsSMILInstanceTimes respectively. + void FilterIntervals(); + void FilterInstanceTimes(InstanceTimeList& aList); + + /** + * Calculates the next acceptable interval for this element after the + * specified interval, or, if no previous interval is specified, it will be + * the first interval with an end time after t=0. + * + * @see SMILANIM 3.6.8 + * + * @param aPrevInterval The previous interval used. If supplied, the first + * interval that begins after aPrevInterval will be + * returned. May be nullptr. + * @param aReplacedInterval The interval that is being updated (if any). This + * used to ensure we don't return interval endpoints + * that are dependent on themselves. May be nullptr. + * @param aFixedBeginTime The time to use for the start of the interval. This + * is used when only the endpoint of the interval + * should be updated such as when the animation is in + * the ACTIVE state. May be nullptr. + * @param[out] aResult The next interval. Will be unchanged if no suitable + * interval was found (in which case false will be + * returned). + * @return true if a suitable interval was found, false otherwise. + */ + bool GetNextInterval(const nsSMILInterval* aPrevInterval, + const nsSMILInterval* aReplacedInterval, + const nsSMILInstanceTime* aFixedBeginTime, + nsSMILInterval& aResult) const; + nsSMILInstanceTime* GetNextGreater(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const; + nsSMILInstanceTime* GetNextGreaterOrEqual(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const; + nsSMILTimeValue CalcActiveEnd(const nsSMILTimeValue& aBegin, + const nsSMILTimeValue& aEnd) const; + nsSMILTimeValue GetRepeatDuration() const; + nsSMILTimeValue ApplyMinAndMax(const nsSMILTimeValue& aDuration) const; + nsSMILTime ActiveTimeToSimpleTime(nsSMILTime aActiveTime, + uint32_t& aRepeatIteration); + nsSMILInstanceTime* CheckForEarlyEnd( + const nsSMILTimeValue& aContainerTime) const; + void UpdateCurrentInterval(bool aForceChangeNotice = false); + void SampleSimpleTime(nsSMILTime aActiveTime); + void SampleFillValue(); + nsresult AddInstanceTimeFromCurrentTime(nsSMILTime aCurrentTime, + double aOffsetSeconds, bool aIsBegin); + void RegisterMilestone(); + bool GetNextMilestone(nsSMILMilestone& aNextMilestone) const; + + // Notification methods. Note that these notifications can result in nested + // calls to this same object. Therefore, + // (i) we should not perform notification until this object is in + // a consistent state to receive callbacks, and + // (ii) after calling these methods we must assume that the state of the + // element may have changed. + void NotifyNewInterval(); + void NotifyChangedInterval(nsSMILInterval* aInterval, + bool aBeginObjectChanged, + bool aEndObjectChanged); + + void FireTimeEventAsync(mozilla::EventMessage aMsg, + int32_t aDetail); + const nsSMILInstanceTime* GetEffectiveBeginInstance() const; + const nsSMILInterval* GetPreviousInterval() const; + bool HasPlayed() const { return !mOldIntervals.IsEmpty(); } + bool HasClientInFillRange() const; + bool EndHasEventConditions() const; + bool AreEndTimesDependentOn( + const nsSMILInstanceTime* aBase) const; + + // Reset the current interval by first passing ownership to a temporary + // variable so that if Unlink() results in us receiving a callback, + // mCurrentInterval will be nullptr and we will be in a consistent state. + void ResetCurrentInterval() + { + if (mCurrentInterval) { + // Transfer ownership to temp var. (This sets mCurrentInterval to null.) + nsAutoPtr interval(mozilla::Move(mCurrentInterval)); + interval->Unlink(); + } + } + + // + // Members + // + mozilla::dom::SVGAnimationElement* mAnimationElement; // [weak] won't outlive + // owner + TimeValueSpecList mBeginSpecs; // [strong] + TimeValueSpecList mEndSpecs; // [strong] + + nsSMILTimeValue mSimpleDur; + + nsSMILRepeatCount mRepeatCount; + nsSMILTimeValue mRepeatDur; + + nsSMILTimeValue mMin; + nsSMILTimeValue mMax; + + enum nsSMILFillMode : uint8_t + { + FILL_REMOVE, + FILL_FREEZE + }; + nsSMILFillMode mFillMode; + static nsAttrValue::EnumTable sFillModeTable[]; + + enum nsSMILRestartMode : uint8_t + { + RESTART_ALWAYS, + RESTART_WHENNOTACTIVE, + RESTART_NEVER + }; + nsSMILRestartMode mRestartMode; + static nsAttrValue::EnumTable sRestartModeTable[]; + + InstanceTimeList mBeginInstances; + InstanceTimeList mEndInstances; + uint32_t mInstanceSerialIndex; + + nsSMILAnimationFunction* mClient; + nsAutoPtr mCurrentInterval; + IntervalList mOldIntervals; + uint32_t mCurrentRepeatIteration; + nsSMILMilestone mPrevRegisteredMilestone; + static const nsSMILMilestone sMaxMilestone; + static const uint8_t sMaxNumIntervals; + static const uint8_t sMaxNumInstanceTimes; + + // Set of dependent time value specs to be notified when establishing a new + // current interval. Change notifications and delete notifications are handled + // by the interval. + // + // [weak] The nsSMILTimeValueSpec objects register themselves and unregister + // on destruction. Likewise, we notify them when we are destroyed. + TimeValueSpecHashSet mTimeDependents; + + /** + * The state of the element in its life-cycle. These states are based on the + * element life-cycle described in SMILANIM 3.6.8 + */ + enum nsSMILElementState + { + STATE_STARTUP, + STATE_WAITING, + STATE_ACTIVE, + STATE_POSTACTIVE + }; + nsSMILElementState mElementState; + + enum nsSMILSeekState + { + SEEK_NOT_SEEKING, + SEEK_FORWARD_FROM_ACTIVE, + SEEK_FORWARD_FROM_INACTIVE, + SEEK_BACKWARD_FROM_ACTIVE, + SEEK_BACKWARD_FROM_INACTIVE + }; + nsSMILSeekState mSeekState; + + // Used to batch updates to the timing model + class AutoIntervalUpdateBatcher; + bool mDeferIntervalUpdates; + bool mDoDeferredUpdate; // Set if an update to the current interval was + // requested while mDeferIntervalUpdates was set + bool mIsDisabled; + + // Stack-based helper class to call UpdateCurrentInterval when it is destroyed + class AutoIntervalUpdater; + + // Recursion depth checking + uint8_t mDeleteCount; + uint8_t mUpdateIntervalRecursionDepth; + static const uint8_t sMaxUpdateIntervalRecursionDepth; +}; + +inline void +ImplCycleCollectionUnlink(nsSMILTimedElement& aField) +{ + aField.Unlink(); +} + +inline void +ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + nsSMILTimedElement& aField, + const char* aName, + uint32_t aFlags = 0) +{ + aField.Traverse(&aCallback); +} + +#endif // NS_SMILTIMEDELEMENT_H_ diff --git a/dom/smil/nsSMILTypes.h b/dom/smil/nsSMILTypes.h new file mode 100644 index 000000000..82153c867 --- /dev/null +++ b/dom/smil/nsSMILTypes.h @@ -0,0 +1,26 @@ +/* -*- 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/. */ + +#ifndef NS_SMILTYPES_H_ +#define NS_SMILTYPES_H_ + +#include + +// A timestamp in milliseconds +// +// A time may represent: +// +// simple time -- offset within the simple duration +// active time -- offset within the active duration +// document time -- offset since the document begin +// wallclock time -- "real" time -- offset since the epoch +// +// For an overview of how this class is related to other SMIL time classes see +// the documentstation in nsSMILTimeValue.h +// +typedef int64_t nsSMILTime; + +#endif // NS_SMILTYPES_H_ diff --git a/dom/smil/nsSMILValue.cpp b/dom/smil/nsSMILValue.cpp new file mode 100644 index 000000000..cd881b0b0 --- /dev/null +++ b/dom/smil/nsSMILValue.cpp @@ -0,0 +1,162 @@ +/* -*- 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 "nsSMILValue.h" +#include "nsDebug.h" +#include + +//---------------------------------------------------------------------- +// Public methods + +nsSMILValue::nsSMILValue(const nsISMILType* aType) + : mType(nsSMILNullType::Singleton()) +{ + if (!aType) { + NS_ERROR("Trying to construct nsSMILValue with null mType pointer"); + return; + } + + InitAndCheckPostcondition(aType); +} + +nsSMILValue::nsSMILValue(const nsSMILValue& aVal) + : mType(nsSMILNullType::Singleton()) +{ + InitAndCheckPostcondition(aVal.mType); + mType->Assign(*this, aVal); +} + +const nsSMILValue& +nsSMILValue::operator=(const nsSMILValue& aVal) +{ + if (&aVal == this) + return *this; + + if (mType != aVal.mType) { + DestroyAndReinit(aVal.mType); + } + + mType->Assign(*this, aVal); + + return *this; +} + +// Move constructor / reassignment operator: +nsSMILValue::nsSMILValue(nsSMILValue&& aVal) + : mU(aVal.mU), // Copying union is only OK because we clear aVal.mType below. + mType(aVal.mType) +{ + // Leave aVal with a null type, so that it's safely destructible (and won't + // mess with anything referenced by its union, which we've copied). + aVal.mType = nsSMILNullType::Singleton(); +} + +nsSMILValue& +nsSMILValue::operator=(nsSMILValue&& aVal) +{ + if (!IsNull()) { + // Clean up any data we're currently tracking. + DestroyAndCheckPostcondition(); + } + + // Copy the union (which could include a pointer to external memory) & mType: + mU = aVal.mU; + mType = aVal.mType; + + // Leave aVal with a null type, so that it's safely destructible (and won't + // mess with anything referenced by its union, which we've now copied). + aVal.mType = nsSMILNullType::Singleton(); + + return *this; +} + +bool +nsSMILValue::operator==(const nsSMILValue& aVal) const +{ + if (&aVal == this) + return true; + + return mType == aVal.mType && mType->IsEqual(*this, aVal); +} + +nsresult +nsSMILValue::Add(const nsSMILValue& aValueToAdd, uint32_t aCount) +{ + if (aValueToAdd.mType != mType) { + NS_ERROR("Trying to add incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->Add(*this, aValueToAdd, aCount); +} + +nsresult +nsSMILValue::SandwichAdd(const nsSMILValue& aValueToAdd) +{ + if (aValueToAdd.mType != mType) { + NS_ERROR("Trying to add incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->SandwichAdd(*this, aValueToAdd); +} + +nsresult +nsSMILValue::ComputeDistance(const nsSMILValue& aTo, double& aDistance) const +{ + if (aTo.mType != mType) { + NS_ERROR("Trying to calculate distance between incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->ComputeDistance(*this, aTo, aDistance); +} + +nsresult +nsSMILValue::Interpolate(const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + if (aEndVal.mType != mType) { + NS_ERROR("Trying to interpolate between incompatible types"); + return NS_ERROR_FAILURE; + } + + if (aResult.mType != mType) { + // Outparam has wrong type + aResult.DestroyAndReinit(mType); + } + + return mType->Interpolate(*this, aEndVal, aUnitDistance, aResult); +} + +//---------------------------------------------------------------------- +// Helper methods + +// Wrappers for nsISMILType::Init & ::Destroy that verify their postconditions +void +nsSMILValue::InitAndCheckPostcondition(const nsISMILType* aNewType) +{ + aNewType->Init(*this); + MOZ_ASSERT(mType == aNewType, + "Post-condition of Init failed. nsSMILValue is invalid"); +} + +void +nsSMILValue::DestroyAndCheckPostcondition() +{ + mType->Destroy(*this); + MOZ_ASSERT(IsNull(), + "Post-condition of Destroy failed. " + "nsSMILValue not null after destroying"); +} + +void +nsSMILValue::DestroyAndReinit(const nsISMILType* aNewType) +{ + DestroyAndCheckPostcondition(); + InitAndCheckPostcondition(aNewType); +} diff --git a/dom/smil/nsSMILValue.h b/dom/smil/nsSMILValue.h new file mode 100644 index 000000000..c0998d61d --- /dev/null +++ b/dom/smil/nsSMILValue.h @@ -0,0 +1,81 @@ +/* -*- 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/. */ + +#ifndef NS_SMILVALUE_H_ +#define NS_SMILVALUE_H_ + +#include "nsISMILType.h" +#include "nsSMILNullType.h" + +/** + * Although objects of this type are generally only created on the stack and + * only exist during the taking of a new time sample, that's not always the + * case. The nsSMILValue objects obtained from attributes' base values are + * cached so that the SMIL engine can make certain optimizations during a + * sample if the base value has not changed since the last sample (potentially + * avoiding recomposing). These nsSMILValue objects typically live much longer + * than a single sample. + */ +class nsSMILValue +{ +public: + nsSMILValue() : mU(), mType(nsSMILNullType::Singleton()) { } + explicit nsSMILValue(const nsISMILType* aType); + nsSMILValue(const nsSMILValue& aVal); + + ~nsSMILValue() + { + mType->Destroy(*this); + } + + const nsSMILValue& operator=(const nsSMILValue& aVal); + + // Move constructor / reassignment operator: + nsSMILValue(nsSMILValue&& aVal); + nsSMILValue& operator=(nsSMILValue&& aVal); + + // Equality operators. These are allowed to be conservative (return false + // more than you'd expect) - see comment above nsISMILType::IsEqual. + bool operator==(const nsSMILValue& aVal) const; + bool operator!=(const nsSMILValue& aVal) const { + return !(*this == aVal); + } + + bool IsNull() const + { + return (mType == nsSMILNullType::Singleton()); + } + + nsresult Add(const nsSMILValue& aValueToAdd, uint32_t aCount = 1); + nsresult SandwichAdd(const nsSMILValue& aValueToAdd); + nsresult ComputeDistance(const nsSMILValue& aTo, double& aDistance) const; + nsresult Interpolate(const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const; + + union { + bool mBool; + uint64_t mUint; + int64_t mInt; + double mDouble; + struct { + float mAngle; + uint16_t mUnit; + uint16_t mOrientType; + } mOrient; + int32_t mIntPair[2]; + float mNumberPair[2]; + void* mPtr; + } mU; + const nsISMILType* mType; + +protected: + void InitAndCheckPostcondition(const nsISMILType* aNewType); + void DestroyAndCheckPostcondition(); + void DestroyAndReinit(const nsISMILType* aNewType); +}; + +#endif // NS_SMILVALUE_H_ diff --git a/dom/smil/test/db_smilAnimateMotion.js b/dom/smil/test/db_smilAnimateMotion.js new file mode 100644 index 000000000..c4dfb4e24 --- /dev/null +++ b/dom/smil/test/db_smilAnimateMotion.js @@ -0,0 +1,253 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* testcase data for */ + +// Fake motion 'attribute', to satisfy testing code that expects an attribute. +var gMotionAttr = new AdditiveAttribute(SMILUtil.getMotionFakeAttributeName(), + "XML", "rect"); + +// CTM-summary-definitions, for re-use by multiple testcase bundles below. +var _reusedCTMLists = { + pacedBasic: { ctm0: [100, 200, 0], + ctm1_6: [105, 205, 0], + ctm1_3: [110, 210, 0], + ctm2_3: [120, 220, 0], + ctm1: [130, 210, 0] + }, + pacedR60: { ctm0: [100, 200, Math.PI/3], + ctm1_6: [105, 205, Math.PI/3], + ctm1_3: [110, 210, Math.PI/3], + ctm2_3: [120, 220, Math.PI/3], + ctm1: [130, 210, Math.PI/3] + }, + pacedRAuto: { ctm0: [100, 200, Math.PI/4], + ctm1_6: [105, 205, Math.PI/4], + ctm1_3: [110, 210, Math.PI/4], + ctm2_3: [120, 220, -Math.PI/4], + ctm1: [130, 210, -Math.PI/4] + }, + pacedRAutoReverse : { ctm0: [100, 200, 5*Math.PI/4], + ctm1_6: [105, 205, 5*Math.PI/4], + ctm1_3: [110, 210, 5*Math.PI/4], + ctm2_3: [120, 220, 3*Math.PI/4], + ctm1: [130, 210, 3*Math.PI/4] + }, + + discreteBasic : { ctm0: [100, 200, 0], + ctm1_6: [100, 200, 0], + ctm1_3: [120, 220, 0], + ctm2_3: [130, 210, 0], + ctm1: [130, 210, 0] + }, + discreteRAuto : { ctm0: [100, 200, Math.PI/4], + ctm1_6: [100, 200, Math.PI/4], + ctm1_3: [120, 220, -Math.PI/4], + ctm2_3: [130, 210, -Math.PI/4], + ctm1: [130, 210, -Math.PI/4] + }, + justMoveBasic : { ctm0: [40, 80, 0], + ctm1_6: [40, 80, 0], + ctm1_3: [40, 80, 0], + ctm2_3: [40, 80, 0], + ctm1: [40, 80, 0] + }, + justMoveR60 : { ctm0: [40, 80, Math.PI/3], + ctm1_6: [40, 80, Math.PI/3], + ctm1_3: [40, 80, Math.PI/3], + ctm2_3: [40, 80, Math.PI/3], + ctm1: [40, 80, Math.PI/3] + }, + justMoveRAuto : { ctm0: [40, 80, Math.atan(2)], + ctm1_6: [40, 80, Math.atan(2)], + ctm1_3: [40, 80, Math.atan(2)], + ctm2_3: [40, 80, Math.atan(2)], + ctm1: [40, 80, Math.atan(2)] + }, + justMoveRAutoReverse : { ctm0: [40, 80, Math.PI + Math.atan(2)], + ctm1_6: [40, 80, Math.PI + Math.atan(2)], + ctm1_3: [40, 80, Math.PI + Math.atan(2)], + ctm2_3: [40, 80, Math.PI + Math.atan(2)], + ctm1: [40, 80, Math.PI + Math.atan(2)] + }, + nullMoveBasic : { ctm0: [0, 0, 0], + ctm1_6: [0, 0, 0], + ctm1_3: [0, 0, 0], + ctm2_3: [0, 0, 0], + ctm1: [0, 0, 0] + }, + nullMoveRAutoReverse : { ctm0: [0, 0, Math.PI], + ctm1_6: [0, 0, Math.PI], + ctm1_3: [0, 0, Math.PI], + ctm2_3: [0, 0, Math.PI], + ctm1: [0, 0, Math.PI] + }, +}; + +var gMotionBundles = +[ + // Bundle to test basic functionality (using default calcMode='paced') + new TestcaseBundle(gMotionAttr, [ + // Basic paced-mode (default) test, with values/mpath/path + new AnimMotionTestcase({ "values": "100, 200; 120, 220; 130, 210" }, + _reusedCTMLists.pacedBasic), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210" }, + _reusedCTMLists.pacedBasic), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210" }, + _reusedCTMLists.pacedBasic), + + // ..and now with rotate=constant value in degrees + new AnimMotionTestcase({ "values": "100,200; 120,220; 130, 210", + "rotate": "60" }, + _reusedCTMLists.pacedR60), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "rotate": "60" }, + _reusedCTMLists.pacedR60), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "rotate": "60" }, + _reusedCTMLists.pacedR60), + + // ..and now with rotate=constant value in radians + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "rotate": "1.0471975512rad" }, // pi/3 + _reusedCTMLists.pacedR60), + + // ..and now with rotate=auto + new AnimMotionTestcase({ "values": "100,200; 120,220; 130, 210", + "rotate": "auto" }, + _reusedCTMLists.pacedRAuto), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "rotate": "auto" }, + _reusedCTMLists.pacedRAuto), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "rotate": "auto" }, + _reusedCTMLists.pacedRAuto), + + // ..and now with rotate=auto-reverse + new AnimMotionTestcase({ "values": "100,200; 120,220; 130, 210", + "rotate": "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "rotate": "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "rotate": "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse), + + ]), + + // Bundle to test calcMode='discrete' + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "100, 200; 120, 220; 130, 210", + "calcMode": "discrete" }, + _reusedCTMLists.discreteBasic), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "calcMode": "discrete" }, + _reusedCTMLists.discreteBasic), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "calcMode": "discrete" }, + _reusedCTMLists.discreteBasic), + // ..and now with rotate=auto + new AnimMotionTestcase({ "values": "100, 200; 120, 220; 130, 210", + "calcMode": "discrete", + "rotate": "auto" }, + _reusedCTMLists.discreteRAuto), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "calcMode": "discrete", + "rotate": "auto" }, + _reusedCTMLists.discreteRAuto), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "calcMode": "discrete", + "rotate": "auto" }, + _reusedCTMLists.discreteRAuto), + ]), + + // Bundle to test relative units ('em') + new TestcaseBundle(gMotionAttr, [ + // First with unitless values from->by... + new AnimMotionTestcase({ "from": "10, 10", + "by": "30, 60" }, + { ctm0: [10, 10, 0], + ctm1_6: [15, 20, 0], + ctm1_3: [20, 30, 0], + ctm2_3: [30, 50, 0], + ctm1: [40, 70, 0] + }), + // ... then add 'em' units (with 1em=10px) on half the values + new AnimMotionTestcase({ "from": "1em, 10", + "by": "30, 6em" }, + { ctm0: [10, 10, 0], + ctm1_6: [15, 20, 0], + ctm1_3: [20, 30, 0], + ctm2_3: [30, 50, 0], + ctm1: [40, 70, 0] + }), + ]), + + // Bundle to test a path with just a "move" command and nothing else + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "40, 80" }, + _reusedCTMLists.justMoveBasic), + new AnimMotionTestcase({ "path": "M40 80" }, + _reusedCTMLists.justMoveBasic), + new AnimMotionTestcase({ "mpath": "m40 80" }, + _reusedCTMLists.justMoveBasic), + ]), + // ... and now with a fixed rotate-angle + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "40, 80", + "rotate": "60" }, + _reusedCTMLists.justMoveR60), + new AnimMotionTestcase({ "path": "M40 80", + "rotate": "60" }, + _reusedCTMLists.justMoveR60), + new AnimMotionTestcase({ "mpath": "m40 80", + "rotate": "60" }, + _reusedCTMLists.justMoveR60), + ]), + // ... and now with 'auto' (should use the move itself as + // our tangent angle, I think) + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "40, 80", + "rotate": "auto" }, + _reusedCTMLists.justMoveRAuto), + new AnimMotionTestcase({ "path": "M40 80", + "rotate": "auto" }, + _reusedCTMLists.justMoveRAuto), + new AnimMotionTestcase({ "mpath": "m40 80", + "rotate": "auto" }, + _reusedCTMLists.justMoveRAuto), + ]), + // ... and now with 'auto-reverse' + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "40, 80", + "rotate": "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse), + new AnimMotionTestcase({ "path": "M40 80", + "rotate": "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse), + new AnimMotionTestcase({ "mpath": "m40 80", + "rotate": "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse), + ]), + // ... and now with a null move to make sure 'auto'/'auto-reverse' don't + // blow up + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "0, 0", + "rotate": "auto" }, + _reusedCTMLists.nullMoveBasic), + ]), + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "0, 0", + "rotate": "auto-reverse" }, + _reusedCTMLists.nullMoveRAutoReverse), + ]), +]; + +// XXXdholbert Add more tests: +// - keyPoints/keyTimes +// - paths with curves +// - Control path with from/by/to diff --git a/dom/smil/test/db_smilCSSFromBy.js b/dom/smil/test/db_smilCSSFromBy.js new file mode 100644 index 000000000..f8b36e70a --- /dev/null +++ b/dom/smil/test/db_smilCSSFromBy.js @@ -0,0 +1,166 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* testcase data for simple "from-by" animations of CSS properties */ + +// NOTE: This js file requires db_smilCSSPropertyList.js + +// Lists of testcases for re-use across multiple properties of the same type +var _fromByTestLists = +{ + color: [ + new AnimTestcaseFromBy("rgb(10, 20, 30)", "currentColor", + { midComp: "rgb(35, 45, 55)", + toComp: "rgb(60, 70, 80)"}), + new AnimTestcaseFromBy("currentColor", "rgb(30, 20, 10)", + { fromComp: "rgb(50, 50, 50)", + midComp: "rgb(65, 60, 55)", + toComp: "rgb(80, 70, 60)"}), + new AnimTestcaseFromBy("rgba(10, 20, 30, 0.2)", "rgba(50, 50, 50, 1)", + // (rgb(10, 20, 30) * 0.2 * 0.5 + rgb(52, 54, 56) * 1.0 * 0.5) * (1 / 0.6) + { midComp: "rgba(45, 48, 52, 0.6)", + // (rgb(10, 20, 30) * 0.2 + rgb(50, 50, 50) * 1) / 1.0 + toComp: "rgb(52, 54, 56)"}), + // Note: technically, the "from" and "by" values in the test case below + // would overflow the maxium color-channel values when added together. + // (e.g. for red [ignoring alpha for now], 100 + 240 = 340 which is > 255) + // The SVG Animation spec says we should clamp color values "as late as + // possible," i.e. allow the channel overflow and clamp at paint-time. + // But for now, we instead clamp the implicit "to" value for the animation + // and interpolate up to that clamped result. + new AnimTestcaseFromBy("rgba(100, 100, 100, 0.6)", "rgba(240, 240, 240, 1)", + // (rgb(100, 100, 100) * 0.6 * 0.5 + rgb(255, 255, 255) * 1.0 * 0.5) * (1 / 0.8) + { midComp: "rgba(197, 197, 197, 0.8)", + // (rgb(100, 100, 100) * 0.6 + rgb(240, 240, 240) is overflowed + toComp: "rgb(255, 255, 255)"}), + ], + lengthNoUnits: [ + new AnimTestcaseFromBy("0", "50", { fromComp: "0px", // 0 acts like 0px + midComp: "25px", + toComp: "50px"}), + new AnimTestcaseFromBy("30", "10", { fromComp: "30px", + midComp: "35px", + toComp: "40px"}), + ], + lengthNoUnitsSVG: [ + new AnimTestcaseFromBy("0", "50", { fromComp: "0", + midComp: "25", + toComp: "50"}), + new AnimTestcaseFromBy("30", "10", { fromComp: "30", + midComp: "35", + toComp: "40"}), + ], + lengthPx: [ + new AnimTestcaseFromBy("0px", "8px", { fromComp: "0px", + midComp: "4px", + toComp: "8px"}), + new AnimTestcaseFromBy("1px", "10px", { midComp: "6px", toComp: "11px"}), + ], + lengthPxSVG: [ + new AnimTestcaseFromBy("0px", "8px", { fromComp: "0", + midComp: "4", + toComp: "8"}), + new AnimTestcaseFromBy("1px", "10px", { fromComp: "1", + midComp: "6", + toComp: "11"}), + ], + opacity: [ + new AnimTestcaseFromBy("1", "-1", { midComp: "0.5", toComp: "0"}), + new AnimTestcaseFromBy("0.4", "-0.6", { midComp: "0.1", toComp: "0"}), + new AnimTestcaseFromBy("0.8", "-1.4", { midComp: "0.1", toComp: "0"}, + "opacities with abs val >1 get clamped too early"), + new AnimTestcaseFromBy("1.2", "-0.6", { midComp: "0.9", toComp: "0.6"}, + "opacities with abs val >1 get clamped too early"), + ], + paint: [ + // The "none" keyword & URI values aren't addiditve, so the animations in + // these testcases are expected to have no effect. + new AnimTestcaseFromBy("none", "none", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "url(#gradB)", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "url(#gradB) red", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "none", { noEffect: 1 }), + new AnimTestcaseFromBy("red", "url(#gradA)", { noEffect: 1 }), + ], + URIsAndNone: [ + // No need to specify { noEffect: 1 }, since plain URI-valued properties + // aren't additive + new AnimTestcaseFromBy("url(#idA)", "url(#idB)"), + new AnimTestcaseFromBy("none", "url(#idB)"), + new AnimTestcaseFromBy("url(#idB)", "inherit"), + ], +}; + +// List of attribute/testcase-list bundles to be tested +var gFromByBundles = +[ + new TestcaseBundle(gPropList.clip, [ + new AnimTestcaseFromBy("rect(1px, 2px, 3px, 4px)", + "rect(10px, 20px, 30px, 40px)", + { midComp: "rect(6px, 12px, 18px, 24px)", + toComp: "rect(11px, 22px, 33px, 44px)"}), + // Adding "auto" (either as a standalone value or a subcomponent value) + // should cause animation to fail. + new AnimTestcaseFromBy("auto", "auto", { noEffect: 1 }), + new AnimTestcaseFromBy("auto", + "rect(auto, auto, auto, auto)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(auto, auto, auto, auto)", + "rect(auto, auto, auto, auto)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(1px, 2px, 3px, 4px)", "auto", { noEffect: 1 }), + new AnimTestcaseFromBy("auto", "rect(1px, 2px, 3px, 4px)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(1px, 2px, 3px, auto)", + "rect(10px, 20px, 30px, 40px)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(1px, auto, 3px, 4px)", + "rect(10px, auto, 30px, 40px)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(1px, 2px, 3px, 4px)", + "rect(10px, auto, 30px, 40px)", { noEffect: 1 }), + ]), + // Check that 'by' animations for 'cursor' has no effect + new TestcaseBundle(gPropList.cursor, [ + new AnimTestcaseFromBy("crosshair", "move"), + ]), + new TestcaseBundle(gPropList.fill, [].concat(_fromByTestLists.color, + _fromByTestLists.paint)), + // Check that 'by' animations involving URIs have no effect + new TestcaseBundle(gPropList.filter, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.font, [ + new AnimTestcaseFromBy("10px serif", + "normal normal 400 100px / 10px monospace"), + ]), + new TestcaseBundle(gPropList.font_size, + [].concat(_fromByTestLists.lengthNoUnits, + _fromByTestLists.lengthPx)), + new TestcaseBundle(gPropList.font_size_adjust, [ + // These testcases implicitly have no effect, because font-size-adjust is + // non-additive (and is declared as such in db_smilCSSPropertyList.js) + new AnimTestcaseFromBy("0.5", "0.1"), + new AnimTestcaseFromBy("none", "0.1"), + new AnimTestcaseFromBy("0.1", "none") + ]), + new TestcaseBundle(gPropList.lighting_color, _fromByTestLists.color), + new TestcaseBundle(gPropList.marker, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_end, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_mid, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_start, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.overflow, [ + new AnimTestcaseFromBy("inherit", "auto"), + new AnimTestcaseFromBy("scroll", "hidden") + ]), + new TestcaseBundle(gPropList.opacity, _fromByTestLists.opacity), + new TestcaseBundle(gPropList.stroke_miterlimit, [ + new AnimTestcaseFromBy("1", "1", { midComp: "1.5", toComp: "2" }), + new AnimTestcaseFromBy("20.1", "-10", { midComp: "15.1", toComp: "10.1" }), + ]), + new TestcaseBundle(gPropList.stroke_dasharray, [ + // These testcases implicitly have no effect, because stroke-dasharray is + // non-additive (and is declared as such in db_smilCSSPropertyList.js) + new AnimTestcaseFromBy("none", "5"), + new AnimTestcaseFromBy("10", "5"), + new AnimTestcaseFromBy("1", "2, 3"), + ]), + new TestcaseBundle(gPropList.stroke_width, + [].concat(_fromByTestLists.lengthNoUnitsSVG, + _fromByTestLists.lengthPxSVG)) +]; diff --git a/dom/smil/test/db_smilCSSFromTo.js b/dom/smil/test/db_smilCSSFromTo.js new file mode 100644 index 000000000..fe9cecd6c --- /dev/null +++ b/dom/smil/test/db_smilCSSFromTo.js @@ -0,0 +1,483 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* testcase data for simple "from-to" animations of CSS properties */ + +// NOTE: This js file requires db_smilCSSPropertyList.js + +// NOTE: I'm Including 'inherit' and 'currentColor' as interpolatable values. +// According to SVG Mobile 1.2 section 16.2.9, "keywords such as inherit which +// yield a numeric computed value may be included in the values list for an +// interpolated animation". + +// Path of test URL (stripping off final slash + filename), for use in +// generating computed value of 'cursor' property +var _testPath = document.URL.substring(0, document.URL.lastIndexOf('/')); + +// Lists of testcases for re-use across multiple properties of the same type +var _fromToTestLists = { + color: [ + new AnimTestcaseFromTo("rgb(100, 100, 100)", "rgb(200, 200, 200)", + { midComp: "rgb(150, 150, 150)" }), + new AnimTestcaseFromTo("#F02000", "#0080A0", + { fromComp: "rgb(240, 32, 0)", + midComp: "rgb(120, 80, 80)", + toComp: "rgb(0, 128, 160)" }), + new AnimTestcaseFromTo("crimson", "lawngreen", + { fromComp: "rgb(220, 20, 60)", + midComp: "rgb(172, 136, 30)", + toComp: "rgb(124, 252, 0)" }), + new AnimTestcaseFromTo("currentColor", "rgb(100, 100, 100)", + { fromComp: "rgb(50, 50, 50)", + midComp: "rgb(75, 75, 75)" }), + new AnimTestcaseFromTo("rgba(10, 20, 30, 0.2)", "rgba(50, 50, 50, 1)", + // (rgb(10, 20, 30) * 0.2 * 0.5 + rgb(50, 50, 50) * 1.0 * 0.5) * (1 / 0.6) + { midComp: "rgba(43, 45, 47, 0.6)", + toComp: "rgb(50, 50, 50)"}), + ], + colorFromInheritBlack: [ + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", + { fromComp: "rgb(0, 0, 0)", + midComp: "rgb(100, 100, 100)" }), + ], + colorFromInheritWhite: [ + new AnimTestcaseFromTo("inherit", "rgb(205, 205, 205)", + { fromComp: "rgb(255, 255, 255)", + midComp: "rgb(230, 230, 230)" }), + ], + paintServer: [ + new AnimTestcaseFromTo("none", "none"), + new AnimTestcaseFromTo("none", "blue", { toComp : "rgb(0, 0, 255)" }), + new AnimTestcaseFromTo("rgb(50, 50, 50)", "none"), + new AnimTestcaseFromTo("url(#gradA)", "url(#gradB) currentColor", + { fromComp: "url(\"" + document.URL + + "#gradA\") rgb(0, 0, 0)", + toComp: "url(\"" + document.URL + + "#gradB\") rgb(50, 50, 50)" }, + "need support for URI-based paints"), + new AnimTestcaseFromTo("url(#gradA) orange", "url(#gradB)", + { fromComp: "url(\"" + document.URL + + "#gradA\") rgb(255, 165, 0)", + toComp: "url(\"" + document.URL + + "#gradB\") rgb(0, 0, 0)" }, + "need support for URI-based paints"), + new AnimTestcaseFromTo("url(#no_grad)", "url(#gradB)", + { fromComp: "url(\"" + document.URL + + "#no_grad\") " + "rgb(0, 0, 0)", + toComp: "url(\"" + document.URL + + "#gradB\") rgb(0, 0, 0)" }, + "need support for URI-based paints"), + new AnimTestcaseFromTo("url(#no_grad) rgb(1,2,3)", "url(#gradB) blue", + { fromComp: "url(\"" + document.URL + + "#no_grad\") " + "rgb(1, 2, 3)", + toComp: "url(\"" + document.URL + + "#gradB\") rgb(0, 0, 255)" }, + "need support for URI-based paints"), + ], + lengthNoUnits: [ + new AnimTestcaseFromTo("0", "20", { fromComp: "0px", + midComp: "10px", + toComp: "20px"}), + new AnimTestcaseFromTo("50", "0", { fromComp: "50px", + midComp: "25px", + toComp: "0px"}), + new AnimTestcaseFromTo("30", "80", { fromComp: "30px", + midComp: "55px", + toComp: "80px"}), + ], + lengthNoUnitsSVG: [ + new AnimTestcaseFromTo("0", "20", { fromComp: "0", + midComp: "10", + toComp: "20"}), + new AnimTestcaseFromTo("50", "0", { fromComp: "50", + midComp: "25", + toComp: "0"}), + new AnimTestcaseFromTo("30", "80", { fromComp: "30", + midComp: "55", + toComp: "80"}), + ], + lengthPx: [ + new AnimTestcaseFromTo("0px", "12px", { fromComp: "0px", + midComp: "6px"}), + new AnimTestcaseFromTo("16px", "0px", { midComp: "8px", + toComp: "0px"}), + new AnimTestcaseFromTo("10px", "20px", { midComp: "15px"}), + new AnimTestcaseFromTo("41px", "1px", { midComp: "21px"}), + ], + lengthPxSVG: [ + new AnimTestcaseFromTo("0px", "12px", { fromComp: "0", + midComp: "6", + toComp: "12"}), + new AnimTestcaseFromTo("16px", "0px", { fromComp: "16", + midComp: "8", + toComp: "0"}), + new AnimTestcaseFromTo("10px", "20px", { fromComp: "10", + midComp: "15", + toComp: "20"}), + new AnimTestcaseFromTo("41px", "1px", { fromComp: "41", + midComp: "21", + toComp: "1"}), + ], + lengthPctSVG: [ + new AnimTestcaseFromTo("20.5%", "0.5%", { midComp: "10.5%" }), + ], + lengthPxPctSVG: [ + new AnimTestcaseFromTo("10px", "10%", { midComp: "15px"}, + "need support for interpolating between " + + "px and percent values"), + ], + lengthPxNoUnitsSVG: [ + new AnimTestcaseFromTo("10", "20px", { fromComp: "10", + midComp: "15", + toComp: "20"}), + new AnimTestcaseFromTo("10px", "20", { fromComp: "10", + midComp: "15", + toComp: "20"}), + ], + opacity: [ + new AnimTestcaseFromTo("1", "0", { midComp: "0.5" }), + new AnimTestcaseFromTo("0.2", "0.12", { midComp: "0.16" }), + new AnimTestcaseFromTo("0.5", "0.7", { midComp: "0.6" }), + new AnimTestcaseFromTo("0.5", "inherit", + { midComp: "0.75", toComp: "1" }), + // Make sure we don't clamp out-of-range values before interpolation + new AnimTestcaseFromTo("0.2", "1.2", + { midComp: "0.7", toComp: "1" }, + "opacities with abs val >1 get clamped too early"), + new AnimTestcaseFromTo("-0.2", "0.6", + { fromComp: "0", midComp: "0.2" }), + new AnimTestcaseFromTo("-1.2", "1.6", + { fromComp: "0", midComp: "0.2", toComp: "1" }, + "opacities with abs val >1 get clamped too early"), + new AnimTestcaseFromTo("-0.6", "1.4", + { fromComp: "0", midComp: "0.4", toComp: "1" }, + "opacities with abs val >1 get clamped too early"), + ], + URIsAndNone: [ + new AnimTestcaseFromTo("url(#idA)", "url(#idB)", + { fromComp: "url(\"#idA\")", + toComp: "url(\"#idB\")"}), + new AnimTestcaseFromTo("none", "url(#idB)", + { toComp: "url(\"#idB\")"}), + new AnimTestcaseFromTo("url(#idB)", "inherit", + { fromComp: "url(\"#idB\")", + toComp: "none"}), + ], +}; + +// List of attribute/testcase-list bundles to be tested +var gFromToBundles = [ + new TestcaseBundle(gPropList.clip, [ + new AnimTestcaseFromTo("rect(1px, 2px, 3px, 4px)", + "rect(11px, 22px, 33px, 44px)", + { midComp: "rect(6px, 12px, 18px, 24px)" }), + new AnimTestcaseFromTo("rect(1px, auto, 3px, 4px)", + "rect(11px, auto, 33px, 44px)", + { midComp: "rect(6px, auto, 18px, 24px)" }), + new AnimTestcaseFromTo("auto", "auto"), + new AnimTestcaseFromTo("rect(auto, auto, auto, auto)", + "rect(auto, auto, auto, auto)"), + // Interpolation not supported in these next cases (with auto --> px-value) + new AnimTestcaseFromTo("rect(1px, auto, 3px, auto)", + "rect(11px, auto, 33px, 44px)"), + new AnimTestcaseFromTo("rect(1px, 2px, 3px, 4px)", + "rect(11px, auto, 33px, 44px)"), + new AnimTestcaseFromTo("rect(1px, 2px, 3px, 4px)", "auto"), + new AnimTestcaseFromTo("auto", "rect(1px, 2px, 3px, 4px)"), + ]), + new TestcaseBundle(gPropList.clip_path, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.clip_rule, [ + new AnimTestcaseFromTo("nonzero", "evenodd"), + new AnimTestcaseFromTo("evenodd", "inherit", { toComp: "nonzero" }), + ]), + new TestcaseBundle(gPropList.color, + [].concat(_fromToTestLists.color, [ + // Note: inherited value is rgb(50, 50, 50) (set on ) + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", + { fromComp: "rgb(50, 50, 50)", + midComp: "rgb(125, 125, 125)" }), + ])), + new TestcaseBundle(gPropList.color_interpolation, [ + new AnimTestcaseFromTo("sRGB", "auto", { fromComp: "srgb" }), + new AnimTestcaseFromTo("inherit", "linearRGB", + { fromComp: "srgb", toComp: "linearrgb" }), + ]), + new TestcaseBundle(gPropList.color_interpolation_filters, [ + new AnimTestcaseFromTo("sRGB", "auto", { fromComp: "srgb" }), + new AnimTestcaseFromTo("auto", "inherit", + { toComp: "linearrgb" }), + ]), + new TestcaseBundle(gPropList.cursor, [ + new AnimTestcaseFromTo("crosshair", "move"), + new AnimTestcaseFromTo("url('a.cur'), url('b.cur'), nw-resize", "sw-resize", + { fromComp: "url(\"" + _testPath + "/a.cur\"), " + + "url(\"" + _testPath + "/b.cur\"), " + + "nw-resize"}), + ]), + new TestcaseBundle(gPropList.direction, [ + new AnimTestcaseFromTo("ltr", "rtl"), + new AnimTestcaseFromTo("rtl", "inherit"), + ]), + new TestcaseBundle(gPropList.display, [ + // I'm not testing the "inherit" value for "display", because part of + // my test runs with "display: none" on everything, and so the + // inherited value isn't always the same. (i.e. the computed value + // of 'inherit' will be different in different tests) + new AnimTestcaseFromTo("block", "table-cell"), + new AnimTestcaseFromTo("inline", "inline-table"), + new AnimTestcaseFromTo("table-row", "none"), + ]), + new TestcaseBundle(gPropList.dominant_baseline, [ + new AnimTestcaseFromTo("use-script", "no-change"), + new AnimTestcaseFromTo("reset-size", "ideographic"), + new AnimTestcaseFromTo("alphabetic", "hanging"), + new AnimTestcaseFromTo("mathematical", "central"), + new AnimTestcaseFromTo("middle", "text-after-edge"), + new AnimTestcaseFromTo("text-before-edge", "auto"), + new AnimTestcaseFromTo("use-script", "inherit", { toComp: "auto" } ), + ]), + // NOTE: Mozilla doesn't currently support "enable-background", but I'm + // testing it here in case we ever add support for it, because it's + // explicitly not animatable in the SVG spec. + new TestcaseBundle(gPropList.enable_background, [ + new AnimTestcaseFromTo("new", "accumulate"), + ]), + new TestcaseBundle(gPropList.fill, + [].concat(_fromToTestLists.color, + _fromToTestLists.paintServer, + _fromToTestLists.colorFromInheritBlack)), + new TestcaseBundle(gPropList.fill_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.fill_rule, [ + new AnimTestcaseFromTo("nonzero", "evenodd"), + new AnimTestcaseFromTo("evenodd", "inherit", { toComp: "nonzero" }), + ]), + new TestcaseBundle(gPropList.filter, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.flood_color, + [].concat(_fromToTestLists.color, + _fromToTestLists.colorFromInheritBlack)), + new TestcaseBundle(gPropList.flood_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.font, [ + // NOTE: 'line-height' is hard-wired at 10px in test_smilCSSFromTo.xhtml + // because if it's not explicitly set, its value varies across platforms. + // NOTE: System font values can't be tested here, because their computed + // values vary from platform to platform. However, they are tested + // visually, in the reftest "anim-css-font-1.svg" + new AnimTestcaseFromTo("10px serif", "30px serif", + { fromComp: "normal normal 400 10px / 10px serif", + toComp: "normal normal 400 30px / 10px serif"}), + new AnimTestcaseFromTo("10px serif", "30px sans-serif", + { fromComp: "normal normal 400 10px / 10px serif", + toComp: "normal normal 400 30px / 10px sans-serif"}), + new AnimTestcaseFromTo("1px / 90px cursive", "100px monospace", + { fromComp: "normal normal 400 1px / 10px cursive", + toComp: "normal normal 400 100px / 10px monospace"}), + new AnimTestcaseFromTo("italic small-caps 200 1px cursive", + "100px monospace", + { fromComp: "italic small-caps 200 1px / 10px cursive", + toComp: "normal normal 400 100px / 10px monospace"}), + new AnimTestcaseFromTo("oblique normal 200 30px / 10px cursive", + "normal small-caps 800 40px / 10px serif"), + ]), + new TestcaseBundle(gPropList.font_family, [ + new AnimTestcaseFromTo("serif", "sans-serif"), + new AnimTestcaseFromTo("cursive", "monospace"), + ]), + new TestcaseBundle(gPropList.font_size, + [].concat(_fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, [ + new AnimTestcaseFromTo("10px", "40%", { midComp: "15px", toComp: "20px" }), + new AnimTestcaseFromTo("160%", "80%", + { fromComp: "80px", + midComp: "60px", + toComp: "40px"}), + ])), + new TestcaseBundle(gPropList.font_size_adjust, [ + new AnimTestcaseFromTo("0.9", "0.1", { midComp: "0.5" }), + new AnimTestcaseFromTo("0.5", "0.6", { midComp: "0.55" }), + new AnimTestcaseFromTo("none", "0.4"), + ]), + new TestcaseBundle(gPropList.font_stretch, [ + new AnimTestcaseFromTo("normal", "wider", {}, + "need support for animating between " + + "relative 'font-stretch' values"), + new AnimTestcaseFromTo("narrower", "ultra-condensed", {}, + "need support for animating between " + + "relative 'font-stretch' values"), + new AnimTestcaseFromTo("ultra-condensed", "condensed", + { midComp: "extra-condensed" }), + new AnimTestcaseFromTo("semi-condensed", "semi-expanded", + { midComp: "normal" }), + new AnimTestcaseFromTo("expanded", "ultra-expanded", + { midComp: "extra-expanded" }), + new AnimTestcaseFromTo("ultra-expanded", "inherit", + { midComp: "expanded", toComp: "normal" }), + ]), + new TestcaseBundle(gPropList.font_style, [ + new AnimTestcaseFromTo("italic", "inherit", { toComp: "normal" }), + new AnimTestcaseFromTo("normal", "italic"), + new AnimTestcaseFromTo("italic", "oblique"), + new AnimTestcaseFromTo("oblique", "normal"), + ]), + new TestcaseBundle(gPropList.font_variant, [ + new AnimTestcaseFromTo("inherit", "small-caps", { fromComp: "normal" }), + new AnimTestcaseFromTo("small-caps", "normal"), + ]), + new TestcaseBundle(gPropList.font_weight, [ + new AnimTestcaseFromTo("100", "900", { midComp: "500" }), + new AnimTestcaseFromTo("700", "100", { midComp: "400" }), + new AnimTestcaseFromTo("inherit", "200", + { fromComp: "400", midComp: "300" }), + new AnimTestcaseFromTo("normal", "bold", + { fromComp: "400", midComp: "500", toComp: "700" }), + new AnimTestcaseFromTo("lighter", "bolder", {}, + "need support for animating between " + + "relative 'font-weight' values"), + ]), + // NOTE: Mozilla doesn't currently support "glyph-orientation-horizontal" or + // "glyph-orientation-vertical", but I'm testing them here in case we ever + // add support for them, because they're explicitly not animatable in the SVG + // spec. + new TestcaseBundle(gPropList.glyph_orientation_horizontal, + [ new AnimTestcaseFromTo("45deg", "60deg") ]), + new TestcaseBundle(gPropList.glyph_orientation_vertical, + [ new AnimTestcaseFromTo("45deg", "60deg") ]), + new TestcaseBundle(gPropList.image_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeQuality", + { toComp: "optimizequality" }), + new AnimTestcaseFromTo("optimizeQuality", "optimizeSpeed", + { fromComp: "optimizequality", + toComp: "optimizespeed" }), + ]), + new TestcaseBundle(gPropList.letter_spacing, + [].concat(_fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG)), + new TestcaseBundle(gPropList.letter_spacing, + _fromToTestLists.lengthPctSVG, + "pct->pct animations don't currently work for " + + "*-spacing properties"), + new TestcaseBundle(gPropList.lighting_color, + [].concat(_fromToTestLists.color, + _fromToTestLists.colorFromInheritWhite)), + new TestcaseBundle(gPropList.marker, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_end, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_mid, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_start, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.mask, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.overflow, [ + new AnimTestcaseFromTo("auto", "visible"), + new AnimTestcaseFromTo("inherit", "visible", { fromComp: "hidden" }), + new AnimTestcaseFromTo("scroll", "auto"), + ]), + new TestcaseBundle(gPropList.pointer_events, [ + new AnimTestcaseFromTo("visibleFill", "stroke", + { fromComp: "visiblefill" }), + new AnimTestcaseFromTo("none", "visibleStroke", + { toComp: "visiblestroke" }), + ]), + new TestcaseBundle(gPropList.shape_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeSpeed", + { toComp: "optimizespeed" }), + new AnimTestcaseFromTo("crispEdges", "geometricPrecision", + { fromComp: "crispedges", + toComp: "geometricprecision" }), + ]), + new TestcaseBundle(gPropList.stop_color, + [].concat(_fromToTestLists.color, + _fromToTestLists.colorFromInheritBlack)), + new TestcaseBundle(gPropList.stop_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.stroke, + [].concat(_fromToTestLists.color, + _fromToTestLists.paintServer, [ + // Note: inherited value is "none" (the default for "stroke" property) + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", + { fromComp: "none"})])), + new TestcaseBundle(gPropList.stroke_dasharray, + [].concat(_fromToTestLists.lengthPctSVG, + [ + new AnimTestcaseFromTo("inherit", "20", { fromComp: "none"}), + new AnimTestcaseFromTo("1", "none"), + new AnimTestcaseFromTo("10", "20", { midComp: "15"}), + new AnimTestcaseFromTo("1", "2, 3", { fromComp: "1, 1", + midComp: "1.5, 2"}), + new AnimTestcaseFromTo("2, 8", "6", { midComp: "4, 7"}), + new AnimTestcaseFromTo("1, 3", "1, 3, 5, 7, 9", + { fromComp: "1, 3, 1, 3, 1, 3, 1, 3, 1, 3", + midComp: "1, 3, 3, 5, 5, 2, 2, 4, 4, 6"}), + ])), + new TestcaseBundle(gPropList.stroke_dashoffset, + [].concat(_fromToTestLists.lengthNoUnitsSVG, + _fromToTestLists.lengthPxSVG, + _fromToTestLists.lengthPxPctSVG, + _fromToTestLists.lengthPctSVG, + _fromToTestLists.lengthPxNoUnitsSVG)), + new TestcaseBundle(gPropList.stroke_linecap, [ + new AnimTestcaseFromTo("butt", "round"), + new AnimTestcaseFromTo("round", "square"), + ]), + new TestcaseBundle(gPropList.stroke_linejoin, [ + new AnimTestcaseFromTo("miter", "round"), + new AnimTestcaseFromTo("round", "bevel"), + ]), + new TestcaseBundle(gPropList.stroke_miterlimit, [ + new AnimTestcaseFromTo("1", "2", { midComp: "1.5" }), + new AnimTestcaseFromTo("20.1", "10.1", { midComp: "15.1" }), + ]), + new TestcaseBundle(gPropList.stroke_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.stroke_width, + [].concat(_fromToTestLists.lengthNoUnitsSVG, + _fromToTestLists.lengthPxSVG, + _fromToTestLists.lengthPxPctSVG, + _fromToTestLists.lengthPctSVG, + _fromToTestLists.lengthPxNoUnitsSVG, [ + new AnimTestcaseFromTo("inherit", "7px", + { fromComp: "1", midComp: "4", toComp: "7" }), + ])), + new TestcaseBundle(gPropList.text_anchor, [ + new AnimTestcaseFromTo("start", "middle"), + new AnimTestcaseFromTo("middle", "end"), + ]), + new TestcaseBundle(gPropList.text_decoration, [ + new AnimTestcaseFromTo("none", "underline"), + new AnimTestcaseFromTo("overline", "line-through"), + new AnimTestcaseFromTo("blink", "underline"), + ]), + new TestcaseBundle(gPropList.text_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeSpeed", + { toComp: "optimizespeed" }), + new AnimTestcaseFromTo("optimizeSpeed", "geometricPrecision", + { fromComp: "optimizespeed", + toComp: "geometricprecision" }), + new AnimTestcaseFromTo("geometricPrecision", "optimizeLegibility", + { fromComp: "geometricprecision", + toComp: "optimizelegibility" }), + ]), + new TestcaseBundle(gPropList.unicode_bidi, [ + new AnimTestcaseFromTo("embed", "bidi-override"), + ]), + new TestcaseBundle(gPropList.vector_effect, [ + new AnimTestcaseFromTo("none", "non-scaling-stroke"), + ]), + new TestcaseBundle(gPropList.visibility, [ + new AnimTestcaseFromTo("visible", "hidden"), + new AnimTestcaseFromTo("hidden", "collapse"), + ]), + new TestcaseBundle(gPropList.word_spacing, + [].concat(_fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG)), + new TestcaseBundle(gPropList.word_spacing, + _fromToTestLists.lengthPctSVG, + "pct->pct animations don't currently work for " + + "*-spacing properties"), + // NOTE: Mozilla doesn't currently support "writing-mode", but I'm + // testing it here in case we ever add support for it, because it's + // explicitly not animatable in the SVG spec. + new TestcaseBundle(gPropList.writing_mode, [ + new AnimTestcaseFromTo("lr", "rl"), + ]), +]; diff --git a/dom/smil/test/db_smilCSSPaced.js b/dom/smil/test/db_smilCSSPaced.js new file mode 100644 index 000000000..3f069691f --- /dev/null +++ b/dom/smil/test/db_smilCSSPaced.js @@ -0,0 +1,321 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* vim: set shiftwidth=4 tabstop=4 autoindent cindent noexpandtab: */ +/* 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/. */ + +/* testcase data for paced-mode animations of CSS properties */ + +// Lists of testcases for re-use across multiple properties of the same type +var _pacedTestLists = +{ + color: [ + new AnimTestcasePaced("rgb(2, 4, 6); " + + "rgb(4, 8, 12); " + + "rgb(8, 16, 24)", + { comp0: "rgb(2, 4, 6)", + comp1_6: "rgb(3, 6, 9)", + comp1_3: "rgb(4, 8, 12)", + comp2_3: "rgb(6, 12, 18)", + comp1: "rgb(8, 16, 24)" + }), + new AnimTestcasePaced("rgb(10, 10, 10); " + + "rgb(20, 10, 8); " + + "rgb(20, 30, 4)", + { comp0: "rgb(10, 10, 10)", + comp1_6: "rgb(15, 10, 9)", + comp1_3: "rgb(20, 10, 8)", + comp2_3: "rgb(20, 20, 6)", + comp1: "rgb(20, 30, 4)" + }), + new AnimTestcasePaced("olive; " + // rgb(128, 128, 0) + "currentColor; " + // rgb(50, 50, 50) + "rgb(206, 150, 206)", + { comp0: "rgb(128, 128, 0)", + comp1_6: "rgb(89, 89, 25)", + comp1_3: "rgb(50, 50, 50)", + comp2_3: "rgb(128, 100, 128)", + comp1: "rgb(206, 150, 206)" + }), + // Use the same RGB component values to make + // premultication effect easier to compute. + new AnimTestcasePaced("rgba(20, 40, 60, 0.2); " + + "rgba(20, 40, 60, 0.4); " + + "rgba(20, 40, 60, 0.8)", + { comp0: "rgba(20, 40, 60, 0.2)", + comp1_6: "rgba(20, 40, 60, 0.3)", + comp1_3: "rgba(20, 40, 60, 0.4)", + comp2_3: "rgba(20, 40, 60, 0.6)", + comp1: "rgba(20, 40, 60, 0.8)" + }), + ], + paintServer : [ + // Sanity check: These aren't interpolatable -- they should end up + // ignoring the calcMode="paced" and falling into discrete-mode. + new AnimTestcasePaced("url(#gradA); url(#gradB)", + { + comp0: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp1_6: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp1_3: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp2_3: "url(\"" + document.URL + "#gradB\") rgb(0, 0, 0)", + comp1: "url(\"" + document.URL + "#gradB\") rgb(0, 0, 0)" + }, + "need support for URI-based paints"), + new AnimTestcasePaced("url(#gradA); url(#gradB); url(#gradC)", + { + comp0: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp1_6: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp1_3: "url(\"" + document.URL + "#gradB\") rgb(0, 0, 0)", + comp2_3: "url(\"" + document.URL + "#gradC\") rgb(0, 0, 0)", + comp1: "url(\"" + document.URL + "#gradC\") rgb(0, 0, 0)" + }, + "need support for URI-based paints"), + ], + lengthNoUnits : [ + new AnimTestcasePaced("2; 0; 4", + { comp0: "2px", + comp1_6: "1px", + comp1_3: "0px", + comp2_3: "2px", + comp1: "4px" + }), + new AnimTestcasePaced("10; 12; 8", + { comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px" + }), + ], + lengthNoUnitsSVG : [ + new AnimTestcasePaced("2; 0; 4", + { comp0: "2", + comp1_6: "1", + comp1_3: "0", + comp2_3: "2", + comp1: "4" + }), + new AnimTestcasePaced("10; 12; 8", + { comp0: "10", + comp1_6: "11", + comp1_3: "12", + comp2_3: "10", + comp1: "8" + }), + ], + lengthPx : [ + new AnimTestcasePaced("0px; 2px; 6px", + { comp0: "0px", + comp1_6: "1px", + comp1_3: "2px", + comp2_3: "4px", + comp1: "6px" + }), + new AnimTestcasePaced("10px; 12px; 8px", + { comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px" + }), + ], + lengthPxSVG : [ + new AnimTestcasePaced("0px; 2px; 6px", + { comp0: "0", + comp1_6: "1", + comp1_3: "2", + comp2_3: "4", + comp1: "6" + }), + new AnimTestcasePaced("10px; 12px; 8px", + { comp0: "10", + comp1_6: "11", + comp1_3: "12", + comp2_3: "10", + comp1: "8" + }), + ], + lengthPctSVG : [ + new AnimTestcasePaced("5%; 6%; 4%", + { comp0: "5%", + comp1_6: "5.5%", + comp1_3: "6%", + comp2_3: "5%", + comp1: "4%" + }), + ], + lengthPxPctSVG : [ + new AnimTestcasePaced("0px; 1%; 6px", + { comp0: "0px", + comp1_6: "1px", + comp1_3: "1%", + comp2_3: "4px", + comp1: "6px" + }, + "need support for interpolating between " + + "px and percent values"), + ], + opacity : [ + new AnimTestcasePaced("0; 0.2; 0.6", + { comp0: "0", + comp1_6: "0.1", + comp1_3: "0.2", + comp2_3: "0.4", + comp1: "0.6" + }), + new AnimTestcasePaced("0.7; 1.0; 0.4", + { comp0: "0.7", + comp1_6: "0.85", + comp1_3: "1", + comp2_3: "0.7", + comp1: "0.4" + }), + ], + rect : [ + new AnimTestcasePaced("rect(2px, 4px, 6px, 8px); " + + "rect(4px, 8px, 12px, 16px); " + + "rect(8px, 16px, 24px, 32px)", + { comp0: "rect(2px, 4px, 6px, 8px)", + comp1_6: "rect(3px, 6px, 9px, 12px)", + comp1_3: "rect(4px, 8px, 12px, 16px)", + comp2_3: "rect(6px, 12px, 18px, 24px)", + comp1: "rect(8px, 16px, 24px, 32px)" + }), + new AnimTestcasePaced("rect(10px, 10px, 10px, 10px); " + + "rect(20px, 10px, 50px, 8px); " + + "rect(20px, 30px, 130px, 4px)", + { comp0: "rect(10px, 10px, 10px, 10px)", + comp1_6: "rect(15px, 10px, 30px, 9px)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "rect(20px, 20px, 90px, 6px)", + comp1: "rect(20px, 30px, 130px, 4px)" + }), + new AnimTestcasePaced("rect(10px, auto, 10px, 10px); " + + "rect(20px, auto, 50px, 8px); " + + "rect(40px, auto, 130px, 4px)", + { comp0: "rect(10px, auto, 10px, 10px)", + comp1_6: "rect(15px, auto, 30px, 9px)", + comp1_3: "rect(20px, auto, 50px, 8px)", + comp2_3: "rect(30px, auto, 90px, 6px)", + comp1: "rect(40px, auto, 130px, 4px)" + }), + // Paced-mode animation is not supported in these next few cases + // (Can't compute subcomponent distance between 'auto' & px-values) + new AnimTestcasePaced("rect(10px, 10px, 10px, auto); " + + "rect(20px, 10px, 50px, 8px); " + + "rect(20px, 30px, 130px, 4px)", + { comp0: "rect(10px, 10px, 10px, auto)", + comp1_6: "rect(10px, 10px, 10px, auto)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "rect(20px, 30px, 130px, 4px)", + comp1: "rect(20px, 30px, 130px, 4px)" + }), + new AnimTestcasePaced("rect(10px, 10px, 10px, 10px); " + + "rect(20px, 10px, 50px, 8px); " + + "auto", + { comp0: "rect(10px, 10px, 10px, 10px)", + comp1_6: "rect(10px, 10px, 10px, 10px)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "auto", + comp1: "auto" + }), + new AnimTestcasePaced("auto; " + + "auto; " + + "rect(20px, 30px, 130px, 4px)", + { comp0: "auto", + comp1_6: "auto", + comp1_3: "auto", + comp2_3: "rect(20px, 30px, 130px, 4px)", + comp1: "rect(20px, 30px, 130px, 4px)" + }), + new AnimTestcasePaced("auto; auto; auto", + { comp0: "auto", + comp1_6: "auto", + comp1_3: "auto", + comp2_3: "auto", + comp1: "auto" + }), + ], +}; + +// TODO: test more properties here. +var gPacedBundles = +[ + new TestcaseBundle(gPropList.clip, _pacedTestLists.rect), + new TestcaseBundle(gPropList.color, _pacedTestLists.color), + new TestcaseBundle(gPropList.direction, [ + new AnimTestcasePaced("rtl; ltr; rtl") + ]), + new TestcaseBundle(gPropList.fill, + [].concat(_pacedTestLists.color, + _pacedTestLists.paintServer)), + new TestcaseBundle(gPropList.font_size, + [].concat(_pacedTestLists.lengthNoUnits, + _pacedTestLists.lengthPx, [ + new AnimTestcasePaced("20%; 24%; 16%", + { comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px" + }), + new AnimTestcasePaced("0px; 4%; 6px", + { comp0: "0px", + comp1_6: "1px", + comp1_3: "2px", + comp2_3: "4px", + comp1: "6px" + }), + ]) + ), + new TestcaseBundle(gPropList.font_size_adjust, [ + new AnimTestcasePaced("0.2; 0.6; 0.8", + { comp0: "0.2", + comp1_6: "0.3", + comp1_3: "0.4", + comp2_3: "0.6", + comp1: "0.8" + }), + new AnimTestcasePaced("none; none; 0.5", + { comp0: "none", + comp1_6: "none", + comp1_3: "none", + comp2_3: "0.5", + comp1: "0.5" + }), + ]), + new TestcaseBundle(gPropList.font_family, [ + // Sanity check: 'font-family' isn't interpolatable. It should end up + // ignoring the calcMode="paced" and falling into discrete-mode. + new AnimTestcasePaced("serif; sans-serif; monospace", + { comp0: "serif", + comp1_6: "serif", + comp1_3: "sans-serif", + comp2_3: "monospace", + comp1: "monospace" + }, + "need support for more font properties"), + ]), + new TestcaseBundle(gPropList.opacity, _pacedTestLists.opacity), + new TestcaseBundle(gPropList.stroke_dasharray, + [].concat(_pacedTestLists.lengthPctSVG, [ + new AnimTestcasePaced("7, 7, 7; 7, 10, 3; 1, 2, 3", + { comp0: "7, 7, 7", + comp1_6: "7, 8.5, 5", + comp1_3: "7, 10, 3", + comp2_3: "4, 6, 3", + comp1: "1, 2, 3" + }), + ])), + new TestcaseBundle(gPropList.stroke_dashoffset, + [].concat(_pacedTestLists.lengthNoUnitsSVG, + _pacedTestLists.lengthPxSVG, + _pacedTestLists.lengthPctSVG, + _pacedTestLists.lengthPxPctSVG)), + new TestcaseBundle(gPropList.stroke_width, + [].concat(_pacedTestLists.lengthNoUnitsSVG, + _pacedTestLists.lengthPxSVG, + _pacedTestLists.lengthPctSVG, + _pacedTestLists.lengthPxPctSVG)), + // XXXdholbert TODO: test 'stroke-dasharray' once we support animating it +]; diff --git a/dom/smil/test/db_smilCSSPropertyList.js b/dom/smil/test/db_smilCSSPropertyList.js new file mode 100644 index 000000000..f9f3de62e --- /dev/null +++ b/dom/smil/test/db_smilCSSPropertyList.js @@ -0,0 +1,93 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* list of CSS properties recognized by SVG 1.1 spec, for use in mochitests */ + +// List of CSS Properties from SVG 1.1 Specification, Appendix N +var gPropList = +{ + // NOTE: AnimatedAttribute signature is: + // (attrName, attrType, sampleTarget, isAnimatable, isAdditive) + + // SKIP 'alignment-baseline' property: animatable but not supported by Mozilla + // SKIP 'baseline-shift' property: animatable but not supported by Mozilla + clip: new AdditiveAttribute("clip", "CSS", "marker"), + clip_path: new NonAdditiveAttribute("clip-path", "CSS", "rect"), + clip_rule: new NonAdditiveAttribute("clip-rule", "CSS", "circle"), + color: new AdditiveAttribute("color", "CSS", "rect"), + color_interpolation: + new NonAdditiveAttribute("color-interpolation", "CSS", "rect"), + color_interpolation_filters: + new NonAdditiveAttribute("color-interpolation-filters", "CSS", + "feFlood"), + // SKIP 'color-profile' property: animatable but not supported by Mozilla + // SKIP 'color-rendering' property: animatable but not supported by Mozilla + cursor: new NonAdditiveAttribute("cursor", "CSS", "rect"), + direction: new NonAnimatableAttribute("direction", "CSS", "text"), + display: new NonAdditiveAttribute("display", "CSS", "rect"), + dominant_baseline: + new NonAdditiveAttribute("dominant-baseline", "CSS", "text"), + enable_background: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + new NonAnimatableAttribute("enable-background", "CSS", "marker"), + fill: new AdditiveAttribute("fill", "CSS", "rect"), + fill_opacity: new AdditiveAttribute("fill-opacity", "CSS", "rect"), + fill_rule: new NonAdditiveAttribute("fill-rule", "CSS", "rect"), + filter: new NonAdditiveAttribute("filter", "CSS", "rect"), + flood_color: new AdditiveAttribute("flood-color", "CSS", "feFlood"), + flood_opacity: new AdditiveAttribute("flood-opacity", "CSS", "feFlood"), + font: new NonAdditiveAttribute("font", "CSS", "text"), + font_family: new NonAdditiveAttribute("font-family", "CSS", "text"), + font_size: new AdditiveAttribute("font-size", "CSS", "text"), + font_size_adjust: + new NonAdditiveAttribute("font-size-adjust", "CSS", "text"), + font_stretch: new NonAdditiveAttribute("font-stretch", "CSS", "text"), + font_style: new NonAdditiveAttribute("font-style", "CSS", "text"), + font_variant: new NonAdditiveAttribute("font-variant", "CSS", "text"), + // XXXdholbert should 'font-weight' be additive? + font_weight: new NonAdditiveAttribute("font-weight", "CSS", "text"), + glyph_orientation_horizontal: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "CSS", "text"), + glyph_orientation_vertical: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "CSS", "text"), + image_rendering: + NonAdditiveAttribute("image-rendering", "CSS", "image"), + // SKIP 'kerning' property: animatable but not supported by Mozilla + letter_spacing: new AdditiveAttribute("letter-spacing", "CSS", "text"), + lighting_color: + new AdditiveAttribute("lighting-color", "CSS", "feDiffuseLighting"), + marker: new NonAdditiveAttribute("marker", "CSS", "line"), + marker_end: new NonAdditiveAttribute("marker-end", "CSS", "line"), + marker_mid: new NonAdditiveAttribute("marker-mid", "CSS", "line"), + marker_start: new NonAdditiveAttribute("marker-start", "CSS", "line"), + mask: new NonAdditiveAttribute("mask", "CSS", "line"), + opacity: new AdditiveAttribute("opacity", "CSS", "rect"), + overflow: new NonAdditiveAttribute("overflow", "CSS", "marker"), + pointer_events: new NonAdditiveAttribute("pointer-events", "CSS", "rect"), + shape_rendering: new NonAdditiveAttribute("shape-rendering", "CSS", "rect"), + stop_color: new AdditiveAttribute("stop-color", "CSS", "stop"), + stop_opacity: new AdditiveAttribute("stop-opacity", "CSS", "stop"), + stroke: new AdditiveAttribute("stroke", "CSS", "rect"), + stroke_dasharray: new NonAdditiveAttribute("stroke-dasharray", "CSS", "rect"), + stroke_dashoffset: new AdditiveAttribute("stroke-dashoffset", "CSS", "rect"), + stroke_linecap: new NonAdditiveAttribute("stroke-linecap", "CSS", "rect"), + stroke_linejoin: new NonAdditiveAttribute("stroke-linejoin", "CSS", "rect"), + stroke_miterlimit: new AdditiveAttribute("stroke-miterlimit", "CSS", "rect"), + stroke_opacity: new AdditiveAttribute("stroke-opacity", "CSS", "rect"), + stroke_width: new AdditiveAttribute("stroke-width", "CSS", "rect"), + text_anchor: new NonAdditiveAttribute("text-anchor", "CSS", "text"), + text_decoration: new NonAdditiveAttribute("text-decoration", "CSS", "text"), + text_rendering: new NonAdditiveAttribute("text-rendering", "CSS", "text"), + unicode_bidi: new NonAnimatableAttribute("unicode-bidi", "CSS", "text"), + vector_effect: new NonAdditiveAttribute("vector-effect", "CSS", "rect"), + visibility: new NonAdditiveAttribute("visibility", "CSS", "rect"), + word_spacing: new AdditiveAttribute("word-spacing", "CSS", "text"), + writing_mode: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + new NonAnimatableAttribute("writing-mode", "CSS", "text"), +}; diff --git a/dom/smil/test/db_smilMappedAttrList.js b/dom/smil/test/db_smilMappedAttrList.js new file mode 100644 index 000000000..ede5dc23b --- /dev/null +++ b/dom/smil/test/db_smilMappedAttrList.js @@ -0,0 +1,131 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* List of SVG presentational attributes in the SVG 1.1 spec, for use in + mochitests. (These are the attributes that are mapped to CSS properties) */ + +var gMappedAttrList = +{ + // NOTE: The list here should match the MappedAttributeEntry arrays in + // nsSVGElement.cpp + + // PresentationAttributes-FillStroke + fill: new AdditiveAttribute("fill", "XML", "rect"), + fill_opacity: new AdditiveAttribute("fill-opacity", "XML", "rect"), + fill_rule: new NonAdditiveAttribute("fill-rule", "XML", "rect"), + stroke: new AdditiveAttribute("stroke", "XML", "rect"), + stroke_dasharray: + new NonAdditiveAttribute("stroke-dasharray", "XML", "rect"), + stroke_dashoffset: new AdditiveAttribute("stroke-dashoffset", "XML", "rect"), + stroke_linecap: new NonAdditiveAttribute("stroke-linecap", "XML", "rect"), + stroke_linejoin: new NonAdditiveAttribute("stroke-linejoin", "XML", "rect"), + stroke_miterlimit: new AdditiveAttribute("stroke-miterlimit", "XML", "rect"), + stroke_opacity: new AdditiveAttribute("stroke-opacity", "XML", "rect"), + stroke_width: new AdditiveAttribute("stroke-width", "XML", "rect"), + + // PresentationAttributes-Graphics + clip_path: new NonAdditiveAttribute("clip-path", "XML", "rect"), + clip_rule: new NonAdditiveAttribute("clip-rule", "XML", "circle"), + color_interpolation: + new NonAdditiveAttribute("color-interpolation", "XML", "rect"), + cursor: new NonAdditiveAttribute("cursor", "XML", "rect"), + display: new NonAdditiveAttribute("display", "XML", "rect"), + filter: new NonAdditiveAttribute("filter", "XML", "rect"), + image_rendering: + NonAdditiveAttribute("image-rendering", "XML", "image"), + mask: new NonAdditiveAttribute("mask", "XML", "line"), + pointer_events: new NonAdditiveAttribute("pointer-events", "XML", "rect"), + shape_rendering: new NonAdditiveAttribute("shape-rendering", "XML", "rect"), + text_rendering: new NonAdditiveAttribute("text-rendering", "XML", "text"), + visibility: new NonAdditiveAttribute("visibility", "XML", "rect"), + + // PresentationAttributes-TextContentElements + // SKIP 'alignment-baseline' property: animatable but not supported by Mozilla + // SKIP 'baseline-shift' property: animatable but not supported by Mozilla + direction: new NonAnimatableAttribute("direction", "XML", "text"), + dominant_baseline: + new NonAdditiveAttribute("dominant-baseline", "XML", "text"), + glyph_orientation_horizontal: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "XML", "text"), + glyph_orientation_vertical: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "XML", "text"), + // SKIP 'kerning' property: animatable but not supported by Mozilla + letter_spacing: new AdditiveAttribute("letter-spacing", "XML", "text"), + text_anchor: new NonAdditiveAttribute("text-anchor", "XML", "text"), + text_decoration: new NonAdditiveAttribute("text-decoration", "XML", "text"), + unicode_bidi: new NonAnimatableAttribute("unicode-bidi", "XML", "text"), + word_spacing: new AdditiveAttribute("word-spacing", "XML", "text"), + + // PresentationAttributes-FontSpecification + font_family: new NonAdditiveAttribute("font-family", "XML", "text"), + font_size: new AdditiveAttribute("font-size", "XML", "text"), + font_size_adjust: + new NonAdditiveAttribute("font-size-adjust", "XML", "text"), + font_stretch: new NonAdditiveAttribute("font-stretch", "XML", "text"), + font_style: new NonAdditiveAttribute("font-style", "XML", "text"), + font_variant: new NonAdditiveAttribute("font-variant", "XML", "text"), + font_weight: new NonAdditiveAttribute("font-weight", "XML", "text"), + + // PresentationAttributes-GradientStop + stop_color: new AdditiveAttribute("stop-color", "XML", "stop"), + stop_opacity: new AdditiveAttribute("stop-opacity", "XML", "stop"), + + // PresentationAttributes-Viewports + overflow: new NonAdditiveAttribute("overflow", "XML", "marker"), + clip: new AdditiveAttribute("clip", "XML", "marker"), + + // PresentationAttributes-Makers + marker_end: new NonAdditiveAttribute("marker-end", "XML", "line"), + marker_mid: new NonAdditiveAttribute("marker-mid", "XML", "line"), + marker_start: new NonAdditiveAttribute("marker-start", "XML", "line"), + + // PresentationAttributes-Color + color: new AdditiveAttribute("color", "XML", "rect"), + + // PresentationAttributes-Filters + color_interpolation_filters: + new NonAdditiveAttribute("color-interpolation-filters", "XML", + "feFlood"), + + // PresentationAttributes-feFlood + flood_color: new AdditiveAttribute("flood-color", "XML", "feFlood"), + flood_opacity: new AdditiveAttribute("flood-opacity", "XML", "feFlood"), + + // PresentationAttributes-LightingEffects + lighting_color: + new AdditiveAttribute("lighting-color", "XML", "feDiffuseLighting"), +}; + +// Utility method to copy a list of TestcaseBundle objects for CSS properties +// into a list of TestcaseBundles for the corresponding mapped attributes. +function convertCSSBundlesToMappedAttr(bundleList) { + // Create mapping of property names to the corresponding + // mapped-attribute object in gMappedAttrList. + var propertyNameToMappedAttr = {}; + for (attributeLabel in gMappedAttrList) { + var propName = gMappedAttrList[attributeLabel].attrName; + propertyNameToMappedAttr[propName] = gMappedAttrList[attributeLabel]; + } + + var convertedBundles = []; + for (var bundleIdx in bundleList) { + var origBundle = bundleList[bundleIdx]; + var propName = origBundle.animatedAttribute.attrName; + if (propertyNameToMappedAttr[propName]) { + // There's a mapped attribute by this name! Duplicate the TestcaseBundle, + // using the Mapped Attribute instead of the CSS Property. + is(origBundle.animatedAttribute.attrType, "CSS", + "expecting to be converting from CSS to XML"); + convertedBundles.push( + new TestcaseBundle(propertyNameToMappedAttr[propName], + origBundle.testcaseList, + origBundle.skipReason)); + } + } + return convertedBundles; +} diff --git a/dom/smil/test/mochitest.ini b/dom/smil/test/mochitest.ini new file mode 100644 index 000000000..b5a0c51bb --- /dev/null +++ b/dom/smil/test/mochitest.ini @@ -0,0 +1,60 @@ +[DEFAULT] +support-files = + db_smilAnimateMotion.js + db_smilCSSFromBy.js + db_smilCSSFromTo.js + db_smilCSSPaced.js + db_smilCSSPropertyList.js + db_smilMappedAttrList.js + smilAnimateMotionValueLists.js + smilExtDoc_helper.svg + smilTestUtils.js + smilXHR_helper.svg + +[test_smilAccessKey.xhtml] +[test_smilAnimateMotion.xhtml] +[test_smilAnimateMotionInvalidValues.xhtml] +[test_smilAnimateMotionOverrideRules.xhtml] +[test_smilBackwardsSeeking.xhtml] +[test_smilCSSFontStretchRelative.xhtml] +[test_smilCSSFromBy.xhtml] +[test_smilCSSFromTo.xhtml] +# [test_smilCSSInherit.xhtml] +# disabled until bug 501183 is fixed +[test_smilCSSInvalidValues.xhtml] +[test_smilCSSPaced.xhtml] +[test_smilChangeAfterFrozen.xhtml] +[test_smilConditionalProcessing.html] +[test_smilContainerBinding.xhtml] +[test_smilCrossContainer.xhtml] +[test_smilDynamicDelayedBeginElement.xhtml] +[test_smilExtDoc.xhtml] +skip-if = toolkit == 'android' +[test_smilFillMode.xhtml] +[test_smilGetSimpleDuration.xhtml] +[test_smilGetStartTime.xhtml] +[test_smilHyperlinking.xhtml] +[test_smilInvalidValues.html] +[test_smilKeySplines.xhtml] +[test_smilKeyTimes.xhtml] +[test_smilKeyTimesPacedMode.xhtml] +[test_smilMappedAttrFromBy.xhtml] +[test_smilMappedAttrFromTo.xhtml] +[test_smilMappedAttrPaced.xhtml] +[test_smilMinTiming.html] +[test_smilRepeatDuration.html] +[test_smilRepeatTiming.xhtml] +skip-if = toolkit == 'android' #TIMED_OUT +[test_smilReset.xhtml] +[test_smilRestart.xhtml] +[test_smilSetCurrentTime.xhtml] +[test_smilSync.xhtml] +[test_smilSyncTransform.xhtml] +[test_smilSyncbaseTarget.xhtml] +[test_smilTextZoom.xhtml] +[test_smilTimeEvents.xhtml] +[test_smilTiming.xhtml] +[test_smilTimingZeroIntervals.xhtml] +[test_smilUpdatedInterval.xhtml] +[test_smilValues.xhtml] +[test_smilXHR.xhtml] diff --git a/dom/smil/test/smilAnimateMotionValueLists.js b/dom/smil/test/smilAnimateMotionValueLists.js new file mode 100644 index 000000000..364bc250e --- /dev/null +++ b/dom/smil/test/smilAnimateMotionValueLists.js @@ -0,0 +1,128 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +/* Lists of valid & invalid values for the various attributes */ +const gValidValues = [ + "10 10", + "10 10;", // Trailing semicolons are allowed + "10 10; ", + " 10 10em ", + "1 2 ; 3,4", + "1,2;3,4", + "0 0", + "0,0", +]; + +const gInvalidValues = [ + ";10 10", + "10 10;;", + "1 2 3", + "1 2 3 4", + "1,2;3,4 ,", + ",", " , ", + ";", " ; ", + "a", " a; ", ";a;", + "", " ", + "1,2;3,4,", + "1,,2", + ",1,2", +]; + +const gValidRotate = [ + "10", + "20.1", + "30.5deg", + "0.5rad", + "auto", + "auto-reverse" +]; + +const gInvalidRotate = [ + " 10 ", + " 10deg", + "10 deg", + "10deg ", + "10 rad ", + "aaa", + " 10.1 ", +]; + +const gValidToBy = [ + "0 0", + "1em,2", + "50.3em 0.2in", + " 1,2", + "1 2 " +]; + +const gInvalidToBy = [ + "0 0 0", + "0 0,0", + "0,0,0", + "1emm 2", + "1 2;", + "1 2,", + " 1,2 ,", + "abc", + ",", + "", + "1,,2", + "1,2," +]; + +const gValidPath = [ + "m0 0 L30 30", + "M20,20L10 10", + "M20,20 L30, 30h20", + "m50 50", "M50 50", + "m0 0", "M0, 0" +]; + +// paths must start with at least a valid "M" segment to be valid +const gInvalidPath = [ + "M20in 20", + "h30", + "L50 50", + "abc", +]; + +// paths that at least start with a valid "M" segment are valid - the spec says +// to parse everything up to the first invalid token +const gValidPathWithErrors = [ + "M20 20em", + "m0 0 L30,,30", + "M10 10 L50 50 abc", +]; + +const gValidKeyPoints = [ + "0; 0.5; 1", + "0;.5;1", + "0; 0; 1", + "0; 1; 1", + "0; 0; 1;", // Trailing semicolons are allowed + "0; 0; 1; ", + "0; 0.000; 1", + "0; 0.000001; 1", +]; + +// Should have 3 values to be valid. +// Same as number of keyTimes values +const gInvalidKeyPoints = [ + "0; 1", + "0; 0.5; 0.75; 1", + "0; 1;", + "0", + "1", + "a", + "", + " ", + "0; -0.1; 1", + "0; 1.1; 1", + "0; 0.1; 1.1", + "-0.1; 0.1; 1", + "0; a; 1", + "0;;1", +]; diff --git a/dom/smil/test/smilExtDoc_helper.svg b/dom/smil/test/smilExtDoc_helper.svg new file mode 100644 index 000000000..fbd9d091a --- /dev/null +++ b/dom/smil/test/smilExtDoc_helper.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/dom/smil/test/smilTestUtils.js b/dom/smil/test/smilTestUtils.js new file mode 100644 index 000000000..2304d499b --- /dev/null +++ b/dom/smil/test/smilTestUtils.js @@ -0,0 +1,858 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* 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/. */ + +// Note: Class syntax roughly based on: +// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Inheritance +const SVG_NS = "http://www.w3.org/2000/svg"; +const XLINK_NS = "http://www.w3.org/1999/xlink"; + +const MPATH_TARGET_ID = "smilTestUtilsTestingPath"; + +function extend(child, supertype) +{ + child.prototype.__proto__ = supertype.prototype; +} + +// General Utility Methods +var SMILUtil = +{ + // Returns the first matched node in the document + getSVGRoot : function() + { + return SMILUtil.getFirstElemWithTag("svg"); + }, + + // Returns the first element in the document with the matching tag + getFirstElemWithTag : function(aTargetTag) + { + var elemList = document.getElementsByTagName(aTargetTag); + return (elemList.length == 0 ? null : elemList[0]); + }, + + // Simple wrapper for getComputedStyle + getComputedStyleSimple: function(elem, prop) + { + return window.getComputedStyle(elem, null).getPropertyValue(prop); + }, + + getAttributeValue: function(elem, attr) + { + if (attr.attrName == SMILUtil.getMotionFakeAttributeName()) { + // Fake motion "attribute" -- "computed value" is the element's CTM + return elem.getCTM(); + } + if (attr.attrType == "CSS") { + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); + } + if (attr.attrType == "XML") { + // XXXdholbert This is appropriate for mapped attributes, but not + // for other attributes. + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); + } + }, + + // Smart wrapper for getComputedStyle, which will generate a "fake" computed + // style for recognized shorthand properties (font, font-variant, overflow, marker) + getComputedStyleWrapper : function(elem, propName) + { + // Special cases for shorthand properties (which aren't directly queriable + // via getComputedStyle) + var computedStyle; + if (propName == "font") { + var subProps = ["font-style", "font-variant-caps", "font-weight", + "font-size", "line-height", "font-family"]; + for (var i in subProps) { + var subPropStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + if (subPropStyle) { + if (subProps[i] == "line-height") { + // There needs to be a "/" before line-height + subPropStyle = "/ " + subPropStyle; + } + if (!computedStyle) { + computedStyle = subPropStyle; + } else { + computedStyle = computedStyle + " " + subPropStyle; + } + } + } + } else if (propName == "font-variant") { + // xxx - this isn't completely correct but it's sufficient for what's + // being tested here + computedStyle = SMILUtil.getComputedStyleSimple(elem, "font-variant-caps"); + } else if (propName == "marker") { + var subProps = ["marker-end", "marker-mid", "marker-start"]; + for (var i in subProps) { + if (!computedStyle) { + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + } else { + is(computedStyle, SMILUtil.getComputedStyleSimple(elem, subProps[i]), + "marker sub-properties should match each other " + + "(they shouldn't be individually set)"); + } + } + } else if (propName == "overflow") { + var subProps = ["overflow-x", "overflow-y"]; + for (var i in subProps) { + if (!computedStyle) { + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + } else { + is(computedStyle, SMILUtil.getComputedStyleSimple(elem, subProps[i]), + "overflow sub-properties should match each other " + + "(they shouldn't be individually set)"); + } + } + } else { + computedStyle = SMILUtil.getComputedStyleSimple(elem, propName); + } + return computedStyle; + }, + + getMotionFakeAttributeName : function() { + return "_motion"; + }, +}; + + +var CTMUtil = +{ + CTM_COMPONENTS_ALL : ["a", "b", "c", "d", "e", "f"], + CTM_COMPONENTS_ROTATE : ["a", "b", "c", "d" ], + + // Function to generate a CTM Matrix from a "summary" + // (a 3-tuple containing [tX, tY, theta]) + generateCTM : function(aCtmSummary) + { + if (!aCtmSummary || aCtmSummary.length != 3) { + ok(false, "Unexpected CTM summary tuple length: " + aCtmSummary.length); + } + var tX = aCtmSummary[0]; + var tY = aCtmSummary[1]; + var theta = aCtmSummary[2]; + var cosTheta = Math.cos(theta); + var sinTheta = Math.sin(theta); + var newCtm = { a : cosTheta, c: -sinTheta, e: tX, + b : sinTheta, d: cosTheta, f: tY }; + return newCtm; + }, + + /// Helper for isCtmEqual + isWithinDelta : function(aTestVal, aExpectedVal, aErrMsg, aIsTodo) { + var testFunc = aIsTodo ? todo : ok; + const delta = 0.00001; // allowing margin of error = 10^-5 + ok(aTestVal >= aExpectedVal - delta && + aTestVal <= aExpectedVal + delta, + aErrMsg + " | got: " + aTestVal + ", expected: " + aExpectedVal); + }, + + assertCTMEqual : function(aLeftCtm, aRightCtm, aComponentsToCheck, + aErrMsg, aIsTodo) { + var foundCTMDifference = false; + for (var j in aComponentsToCheck) { + var curComponent = aComponentsToCheck[j]; + if (!aIsTodo) { + CTMUtil.isWithinDelta(aLeftCtm[curComponent], aRightCtm[curComponent], + aErrMsg + " | component: " + curComponent, false); + } else if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { + foundCTMDifference = true; + } + } + + if (aIsTodo) { + todo(!foundCTMDifference, aErrMsg + " | (currently marked todo)"); + } + }, + + assertCTMNotEqual : function(aLeftCtm, aRightCtm, aComponentsToCheck, + aErrMsg, aIsTodo) { + // CTM should not match initial one + var foundCTMDifference = false; + for (var j in aComponentsToCheck) { + var curComponent = aComponentsToCheck[j]; + if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { + foundCTMDifference = true; + break; // We found a difference, as expected. Success! + } + } + + if (aIsTodo) { + todo(foundCTMDifference, aErrMsg + " | (currently marked todo)"); + } else { + ok(foundCTMDifference, aErrMsg); + } + }, +}; + + +// Wrapper for timing information +function SMILTimingData(aBegin, aDur) +{ + this._begin = aBegin; + this._dur = aDur; +} +SMILTimingData.prototype = +{ + _begin: null, + _dur: null, + getBeginTime : function() { return this._begin; }, + getDur : function() { return this._dur; }, + getEndTime : function() { return this._begin + this._dur; }, + getFractionalTime : function(aPortion) + { + return this._begin + aPortion * this._dur; + }, +} + +/** + * Attribute: a container for information about an attribute we'll + * attempt to animate with SMIL in our tests. + * + * See also the factory methods below: NonAnimatableAttribute(), + * NonAdditiveAttribute(), and AdditiveAttribute(). + * + * @param aAttrName The name of the attribute + * @param aAttrType The type of the attribute ("CSS" vs "XML") + * @param aTargetTag The name of an element that this attribute could be + * applied to. + * @param aIsAnimatable A bool indicating whether this attribute is defined as + * animatable in the SVG spec. + * @param aIsAdditive A bool indicating whether this attribute is defined as + * additive (i.e. supports "by" animation) in the SVG spec. + */ +function Attribute(aAttrName, aAttrType, aTargetTag, + aIsAnimatable, aIsAdditive) +{ + this.attrName = aAttrName; + this.attrType = aAttrType; + this.targetTag = aTargetTag; + this.isAnimatable = aIsAnimatable; + this.isAdditive = aIsAdditive; +} +Attribute.prototype = +{ + // Member variables + attrName : null, + attrType : null, + isAnimatable : null, + testcaseList : null, +}; + +// Generators for Attribute objects. These allow lists of attribute +// definitions to be more human-readible than if we were using Attribute() with +// boolean flags, e.g. "Attribute(..., true, true), Attribute(..., true, false) +function NonAnimatableAttribute(aAttrName, aAttrType, aTargetTag) +{ + return new Attribute(aAttrName, aAttrType, aTargetTag, false, false); +} +function NonAdditiveAttribute(aAttrName, aAttrType, aTargetTag) +{ + return new Attribute(aAttrName, aAttrType, aTargetTag, true, false); +} +function AdditiveAttribute(aAttrName, aAttrType, aTargetTag) +{ + return new Attribute(aAttrName, aAttrType, aTargetTag, true, true); +} + +/** + * TestcaseBundle: a container for a group of tests for a particular attribute + * + * @param aAttribute An Attribute object for the attribute + * @param aTestcaseList An array of AnimTestcase objects + */ +function TestcaseBundle(aAttribute, aTestcaseList, aSkipReason) +{ + this.animatedAttribute = aAttribute; + this.testcaseList = aTestcaseList; + this.skipReason = aSkipReason; +} +TestcaseBundle.prototype = +{ + // Member variables + animatedAttribute : null, + testcaseList : null, + skipReason : null, + + // Methods + go : function(aTimingData) { + if (this.skipReason) { + todo(false, "Skipping a bundle for '" + this.animatedAttribute.attrName + + "' because: " + this.skipReason); + } else { + // Sanity Check: Bundle should have > 0 testcases + if (!this.testcaseList || !this.testcaseList.length) { + ok(false, "a bundle for '" + this.animatedAttribute.attrName + + "' has no testcases"); + } + + var targetElem = + SMILUtil.getFirstElemWithTag(this.animatedAttribute.targetTag); + + if (!targetElem) { + ok(false, "Error: can't find an element of type '" + + this.animatedAttribute.targetTag + + "', so I can't test property '" + + this.animatedAttribute.attrName + "'"); + return; + } + + for (var testcaseIdx in this.testcaseList) { + var testcase = this.testcaseList[testcaseIdx]; + if (testcase.skipReason) { + todo(false, "Skipping a testcase for '" + + this.animatedAttribute.attrName + + "' because: " + testcase.skipReason); + } else { + testcase.runTest(targetElem, this.animatedAttribute, + aTimingData, false); + testcase.runTest(targetElem, this.animatedAttribute, + aTimingData, true); + } + } + } + }, +}; + +/** + * AnimTestcase: an abstract class that represents an animation testcase. + * (e.g. a set of "from"/"to" values to test) + */ +function AnimTestcase() {} // abstract => no constructor +AnimTestcase.prototype = +{ + // Member variables + _animElementTagName : "animate", // Can be overridden for e.g. animateColor + computedValMap : null, + skipReason : null, + + // Methods + /** + * runTest: Runs this AnimTestcase + * + * @param aTargetElem The node to be targeted in our test animation. + * @param aTargetAttr An Attribute object representing the attribute + * to be targeted in our test animation. + * @param aTimeData A SMILTimingData object with timing information for + * our test animation. + * @param aIsFreeze If true, indicates that our test animation should use + * fill="freeze"; otherwise, we'll default to fill="remove". + */ + runTest : function(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) + { + // SANITY CHECKS + if (!SMILUtil.getSVGRoot().animationsPaused()) { + ok(false, "Should start each test with animations paused"); + } + if (SMILUtil.getSVGRoot().getCurrentTime() != 0) { + ok(false, "Should start each test at time = 0"); + } + + // SET UP + // Cache initial computed value + var baseVal = SMILUtil.getAttributeValue(aTargetElem, aTargetAttr); + + // Create & append animation element + var anim = this.setupAnimationElement(aTargetAttr, aTimeData, aIsFreeze); + aTargetElem.appendChild(anim); + + // Build a list of [seek-time, expectedValue, errorMessage] triplets + var seekList = this.buildSeekList(aTargetAttr, baseVal, aTimeData, aIsFreeze); + + // DO THE ACTUAL TESTING + this.seekAndTest(seekList, aTargetElem, aTargetAttr); + + // CLEAN UP + aTargetElem.removeChild(anim); + SMILUtil.getSVGRoot().setCurrentTime(0); + }, + + // HELPER FUNCTIONS + // setupAnimationElement: element + // Subclasses should extend this parent method + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + var animElement = document.createElementNS(SVG_NS, + this._animElementTagName); + animElement.setAttribute("attributeName", aAnimAttr.attrName); + animElement.setAttribute("attributeType", aAnimAttr.attrType); + animElement.setAttribute("begin", aTimeData.getBeginTime()); + animElement.setAttribute("dur", aTimeData.getDur()); + if (aIsFreeze) { + animElement.setAttribute("fill", "freeze"); + } + return animElement; + }, + + buildSeekList : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + if (!aAnimAttr.isAnimatable) { + return this.buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, + "defined as non-animatable in SVG spec"); + } + if (this.computedValMap.noEffect) { + return this.buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, + "testcase specified to have no effect"); + } + return this.buildSeekListAnimated(aAnimAttr, aBaseVal, + aTimeData, aIsFreeze) + }, + + seekAndTest : function(aSeekList, aTargetElem, aTargetAttr) + { + var svg = document.getElementById("svg"); + for (var i in aSeekList) { + var entry = aSeekList[i]; + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); + is(SMILUtil.getAttributeValue(aTargetElem, aTargetAttr), + entry[1], entry[2]); + } + }, + + // methods that expect to be overridden in subclasses + buildSeekListStatic : function(aAnimAttr, aBaseVal, + aTimeData, aReasonStatic) {}, + buildSeekListAnimated : function(aAnimAttr, aBaseVal, + aTimeData, aIsFreeze) {}, +}; + + +// Abstract parent class to share code between from-to & from-by testcases. +function AnimTestcaseFrom() {} // abstract => no constructor +AnimTestcaseFrom.prototype = +{ + // Member variables + from : null, + + // Methods + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + // Call super, and then add my own customization + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, + [aAnimAttr, aTimeData, aIsFreeze]); + animElem.setAttribute("from", this.from) + return animElem; + }, + + buildSeekListStatic : function(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) + { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + + ": shouldn't be affected by animation "; + seekList.push([aTimeData.getBeginTime(), aBaseVal, + msgPrefix + "(at animation begin) - " + aReasonStatic]); + seekList.push([aTimeData.getFractionalTime(1/2), aBaseVal, + msgPrefix + "(at animation mid) - " + aReasonStatic]); + seekList.push([aTimeData.getEndTime(), aBaseVal, + msgPrefix + "(at animation end) - " + aReasonStatic]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), aBaseVal, + msgPrefix + "(after animation end) - " + aReasonStatic]); + return seekList; + }, + + buildSeekListAnimated : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + ": "; + if (aTimeData.getBeginTime() > 0.1) { + seekList.push([aTimeData.getBeginTime() - 0.1, + aBaseVal, + msgPrefix + "checking that base value is set " + + "before start of animation"]); + } + + seekList.push([aTimeData.getBeginTime(), + this.computedValMap.fromComp || this.from, + msgPrefix + "checking that 'from' value is set " + + "at start of animation"]); + seekList.push([aTimeData.getFractionalTime(1/2), + this.computedValMap.midComp || + this.computedValMap.toComp || this.to, + msgPrefix + "checking value halfway through animation"]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = this.computedValMap.toComp || this.to; + finalMsg = msgPrefix + "[freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = msgPrefix + + "[remove-mode] checking that animation is cleared "; + } + seekList.push([aTimeData.getEndTime(), + expectedEndVal, finalMsg + "at end of animation"]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, finalMsg + "after end of animation"]); + return seekList; + }, +} +extend(AnimTestcaseFrom, AnimTestcase); + +/* + * A testcase for a simple "from-to" animation + * @param aFrom The 'from' value + * @param aTo The 'to' value + * @param aComputedValMap A hash-map that contains some computed values, + * if they're needed, as follows: + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) + * - midComp: Computed value that we expect to visit halfway through the + * animation (if different from |aTo|) + * - toComp: Computed value version of |aTo| (if different from |aTo|) + * - noEffect: Special flag -- if set, indicates that this testcase is + * expected to have no effect on the computed value. (e.g. the + * given values are invalid.) + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + * + */ +function AnimTestcaseFromTo(aFrom, aTo, aComputedValMap, aSkipReason) +{ + this.from = aFrom; + this.to = aTo; + this.computedValMap = aComputedValMap || {}; // Let aComputedValMap be omitted + this.skipReason = aSkipReason; +} +AnimTestcaseFromTo.prototype = +{ + // Member variables + to : null, + + // Methods + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + // Call super, and then add my own customization + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(this, + [aAnimAttr, aTimeData, aIsFreeze]); + animElem.setAttribute("to", this.to) + return animElem; + }, +} +extend(AnimTestcaseFromTo, AnimTestcaseFrom); + +/* + * A testcase for a simple "from-by" animation. + * + * @param aFrom The 'from' value + * @param aBy The 'by' value + * @param aComputedValMap A hash-map that contains some computed values that + * we expect to visit, as follows: + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) + * - midComp: Computed value that we expect to visit halfway through the + * animation (|aFrom| + |aBy|/2) + * - toComp: Computed value of the animation endpoint (|aFrom| + |aBy|) + * - noEffect: Special flag -- if set, indicates that this testcase is + * expected to have no effect on the computed value. (e.g. the + * given values are invalid. Or the attribute may be animatable + * and additive, but the particular "from" & "by" values that + * are used don't support addition.) + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimTestcaseFromBy(aFrom, aBy, aComputedValMap, aSkipReason) +{ + this.from = aFrom; + this.by = aBy; + this.computedValMap = aComputedValMap; + this.skipReason = aSkipReason; + if (this.computedValMap && + !this.computedValMap.noEffect && !this.computedValMap.toComp) { + ok(false, "AnimTestcaseFromBy needs expected computed final value"); + } +} +AnimTestcaseFromBy.prototype = +{ + // Member variables + by : null, + + // Methods + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + // Call super, and then add my own customization + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(this, + [aAnimAttr, aTimeData, aIsFreeze]); + animElem.setAttribute("by", this.by) + return animElem; + }, + buildSeekList : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + if (!aAnimAttr.isAdditive) { + return this.buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, + "defined as non-additive in SVG spec"); + } + // Just use inherited method + return AnimTestcaseFrom.prototype.buildSeekList.apply(this, + [aAnimAttr, aBaseVal, aTimeData, aIsFreeze]); + }, +} +extend(AnimTestcaseFromBy, AnimTestcaseFrom); + +/* + * A testcase for a "paced-mode" animation + * @param aValues An array of values, to be used as the "Values" list + * @param aComputedValMap A hash-map that contains some computed values, + * if they're needed, as follows: + * - comp0: The computed value at the start of the animation + * - comp1_6: The computed value exactly 1/6 through animation + * - comp1_3: The computed value exactly 1/3 through animation + * - comp2_3: The computed value exactly 2/3 through animation + * - comp1: The computed value of the animation endpoint + * The math works out easiest if... + * (a) aValuesString has 3 entries in its values list: vA, vB, vC + * (b) dist(vB, vC) = 2 * dist(vA, vB) + * With this setup, we can come up with expected intermediate values according + * to the following rules: + * - comp0 should be vA + * - comp1_6 should be us halfway between vA and vB + * - comp1_3 should be vB + * - comp2_3 should be halfway between vB and vC + * - comp1 should be vC + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimTestcasePaced(aValuesString, aComputedValMap, aSkipReason) +{ + this.valuesString = aValuesString; + this.computedValMap = aComputedValMap; + this.skipReason = aSkipReason; + if (this.computedValMap && + (!this.computedValMap.comp0 || + !this.computedValMap.comp1_6 || + !this.computedValMap.comp1_3 || + !this.computedValMap.comp2_3 || + !this.computedValMap.comp1)) { + ok(false, "This AnimTestcasePaced has an incomplete computed value map"); + } +} +AnimTestcasePaced.prototype = +{ + // Member variables + valuesString : null, + + // Methods + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + // Call super, and then add my own customization + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, + [aAnimAttr, aTimeData, aIsFreeze]); + animElem.setAttribute("values", this.valuesString) + animElem.setAttribute("calcMode", "paced"); + return animElem; + }, + buildSeekListAnimated : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + ": checking value "; + seekList.push([aTimeData.getBeginTime(), + this.computedValMap.comp0, + msgPrefix + "at start of animation"]); + seekList.push([aTimeData.getFractionalTime(1/6), + this.computedValMap.comp1_6, + msgPrefix + "1/6 of the way through animation."]); + seekList.push([aTimeData.getFractionalTime(1/3), + this.computedValMap.comp1_3, + msgPrefix + "1/3 of the way through animation."]); + seekList.push([aTimeData.getFractionalTime(2/3), + this.computedValMap.comp2_3, + msgPrefix + "2/3 of the way through animation."]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = this.computedValMap.comp1; + finalMsg = aAnimAttr.attrName + + ": [freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = aAnimAttr.attrName + + ": [remove-mode] checking that animation is cleared "; + } + seekList.push([aTimeData.getEndTime(), + expectedEndVal, finalMsg + "at end of animation"]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, finalMsg + "after end of animation"]); + return seekList; + }, + buildSeekListStatic : function(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) + { + var seekList = new Array(); + var msgPrefix = + aAnimAttr.attrName + ": shouldn't be affected by animation "; + seekList.push([aTimeData.getBeginTime(), aBaseVal, + msgPrefix + "(at animation begin) - " + aReasonStatic]); + seekList.push([aTimeData.getFractionalTime(1/6), aBaseVal, + msgPrefix + "(1/6 of the way through animation) - " + + aReasonStatic]); + seekList.push([aTimeData.getFractionalTime(1/3), aBaseVal, + msgPrefix + "(1/3 of the way through animation) - " + + aReasonStatic]); + seekList.push([aTimeData.getFractionalTime(2/3), aBaseVal, + msgPrefix + "(2/3 of the way through animation) - " + + aReasonStatic]); + seekList.push([aTimeData.getEndTime(), aBaseVal, + msgPrefix + "(at animation end) - " + aReasonStatic]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), aBaseVal, + msgPrefix + "(after animation end) - " + aReasonStatic]); + return seekList; + }, +}; +extend(AnimTestcasePaced, AnimTestcase); + +/* + * A testcase for an animation. + * + * @param aAttrValueHash A hash-map mapping attribute names to values. + * Should include at least 'path', 'values', 'to' + * or 'by' to describe the motion path. + * @param aCtmMap A hash-map that contains summaries of the expected resulting + * CTM at various points during the animation. The CTM is + * summarized as a tuple of three numbers: [tX, tY, theta] + (indicating a translate(tX,tY) followed by a rotate(theta)) + * - ctm0: The CTM summary at the start of the animation + * - ctm1_6: The CTM summary at exactly 1/6 through animation + * - ctm1_3: The CTM summary at exactly 1/3 through animation + * - ctm2_3: The CTM summary at exactly 2/3 through animation + * - ctm1: The CTM summary at the animation endpoint + * + * NOTE: For paced-mode animation (the default for animateMotion), the math + * works out easiest if: + * (a) our motion path has 3 points: vA, vB, vC + * (b) dist(vB, vC) = 2 * dist(vA, vB) + * (See discussion in header comment for AnimTestcasePaced.) + * + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimMotionTestcase(aAttrValueHash, aCtmMap, aSkipReason) +{ + this.attrValueHash = aAttrValueHash; + this.ctmMap = aCtmMap; + this.skipReason = aSkipReason; + if (this.ctmMap && + (!this.ctmMap.ctm0 || + !this.ctmMap.ctm1_6 || + !this.ctmMap.ctm1_3 || + !this.ctmMap.ctm2_3 || + !this.ctmMap.ctm1)) { + ok(false, "This AnimMotionTestcase has an incomplete CTM map"); + } +} +AnimMotionTestcase.prototype = +{ + // Member variables + _animElementTagName : "animateMotion", + + // Implementations of inherited methods that we need to override: + // -------------------------------------------------------------- + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + var animElement = document.createElementNS(SVG_NS, + this._animElementTagName); + animElement.setAttribute("begin", aTimeData.getBeginTime()); + animElement.setAttribute("dur", aTimeData.getDur()); + if (aIsFreeze) { + animElement.setAttribute("fill", "freeze"); + } + for (var attrName in this.attrValueHash) { + if (attrName == "mpath") { + this.createPath(this.attrValueHash[attrName]); + this.createMpath(animElement); + } else { + animElement.setAttribute(attrName, this.attrValueHash[attrName]); + } + } + return animElement; + }, + + createPath : function(aPathDescription) + { + var path = document.createElementNS(SVG_NS, "path"); + path.setAttribute("d", aPathDescription); + path.setAttribute("id", MPATH_TARGET_ID); + return SMILUtil.getSVGRoot().appendChild(path); + }, + + createMpath : function(aAnimElement) + { + var mpath = document.createElementNS(SVG_NS, "mpath"); + mpath.setAttributeNS(XLINK_NS, "href", "#" + MPATH_TARGET_ID); + return aAnimElement.appendChild(mpath); + }, + + // Override inherited seekAndTest method since... + // (a) it expects a computedValMap and we have a computed-CTM map instead + // and (b) it expects we might have no effect (for non-animatable attrs) + buildSeekList : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + var seekList = new Array(); + var msgPrefix = "CTM mismatch "; + seekList.push([aTimeData.getBeginTime(), + CTMUtil.generateCTM(this.ctmMap.ctm0), + msgPrefix + "at start of animation"]); + seekList.push([aTimeData.getFractionalTime(1/6), + CTMUtil.generateCTM(this.ctmMap.ctm1_6), + msgPrefix + "1/6 of the way through animation."]); + seekList.push([aTimeData.getFractionalTime(1/3), + CTMUtil.generateCTM(this.ctmMap.ctm1_3), + msgPrefix + "1/3 of the way through animation."]); + seekList.push([aTimeData.getFractionalTime(2/3), + CTMUtil.generateCTM(this.ctmMap.ctm2_3), + msgPrefix + "2/3 of the way through animation."]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = CTMUtil.generateCTM(this.ctmMap.ctm1); + finalMsg = aAnimAttr.attrName + + ": [freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = aAnimAttr.attrName + + ": [remove-mode] checking that animation is cleared "; + } + seekList.push([aTimeData.getEndTime(), + expectedEndVal, finalMsg + "at end of animation"]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, finalMsg + "after end of animation"]); + return seekList; + }, + + // Override inherited seekAndTest method + // (Have to use assertCTMEqual() instead of is() for comparison, to check each + // component of the CTM and to allow for a small margin of error.) + seekAndTest : function(aSeekList, aTargetElem, aTargetAttr) + { + var svg = document.getElementById("svg"); + for (var i in aSeekList) { + var entry = aSeekList[i]; + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); + CTMUtil.assertCTMEqual(aTargetElem.getCTM(), entry[1], + CTMUtil.CTM_COMPONENTS_ALL, entry[2], false); + } + }, + + // Override "runTest" method so we can remove any element that we + // created at the end of each test. + runTest : function(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) + { + AnimTestcase.prototype.runTest.apply(this, + [aTargetElem, aTargetAttr, aTimeData, aIsFreeze]); + var pathElem = document.getElementById(MPATH_TARGET_ID); + if (pathElem) { + SMILUtil.getSVGRoot().removeChild(pathElem); + } + } +}; +extend(AnimMotionTestcase, AnimTestcase); + +// MAIN METHOD +function testBundleList(aBundleList, aTimingData) +{ + for (var bundleIdx in aBundleList) { + aBundleList[bundleIdx].go(aTimingData); + } +} diff --git a/dom/smil/test/smilXHR_helper.svg b/dom/smil/test/smilXHR_helper.svg new file mode 100644 index 000000000..cb0b51c36 --- /dev/null +++ b/dom/smil/test/smilXHR_helper.svg @@ -0,0 +1,8 @@ + + + + + + diff --git a/dom/smil/test/test_smilAccessKey.xhtml b/dom/smil/test/test_smilAccessKey.xhtml new file mode 100644 index 000000000..ef7e1b73d --- /dev/null +++ b/dom/smil/test/test_smilAccessKey.xhtml @@ -0,0 +1,362 @@ + + + Test for SMIL accessKey support + + + + +Mozilla Bug + 587910 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilAnimateMotion.xhtml b/dom/smil/test/test_smilAnimateMotion.xhtml new file mode 100644 index 000000000..250c0b80f --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotion.xhtml @@ -0,0 +1,51 @@ + + + + Test for animateMotion behavior + + + + + + +Mozilla Bug 436418 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml b/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml new file mode 100644 index 000000000..0554c7fd8 --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml @@ -0,0 +1,176 @@ + + + + Test for animateMotion acceptance of invalid values + + + + + diff --git a/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml b/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml new file mode 100644 index 000000000..a428ff332 --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml @@ -0,0 +1,215 @@ + + + + Test for overriding of path-defining attributes for animateMotion + + + + + diff --git a/dom/smil/test/test_smilBackwardsSeeking.xhtml b/dom/smil/test/test_smilBackwardsSeeking.xhtml new file mode 100644 index 000000000..7a40bf718 --- /dev/null +++ b/dom/smil/test/test_smilBackwardsSeeking.xhtml @@ -0,0 +1,191 @@ + + + Test for backwards seeking behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilCSSFontStretchRelative.xhtml b/dom/smil/test/test_smilCSSFontStretchRelative.xhtml new file mode 100644 index 000000000..08caff267 --- /dev/null +++ b/dom/smil/test/test_smilCSSFontStretchRelative.xhtml @@ -0,0 +1,102 @@ + + + Test for Animation Behavior on CSS Properties + + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilCSSFromBy.xhtml b/dom/smil/test/test_smilCSSFromBy.xhtml new file mode 100644 index 000000000..d6ac7ff0e --- /dev/null +++ b/dom/smil/test/test_smilCSSFromBy.xhtml @@ -0,0 +1,49 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + +

+
+ + + + testing 123 + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilCSSFromTo.xhtml b/dom/smil/test/test_smilCSSFromTo.xhtml new file mode 100644 index 000000000..88f3ad715 --- /dev/null +++ b/dom/smil/test/test_smilCSSFromTo.xhtml @@ -0,0 +1,76 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + +

+
+ + + + testing 123 + + + + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilCSSInherit.xhtml b/dom/smil/test/test_smilCSSInherit.xhtml new file mode 100644 index 000000000..9da18f52b --- /dev/null +++ b/dom/smil/test/test_smilCSSInherit.xhtml @@ -0,0 +1,85 @@ + + + Test for Animation Behavior on CSS Properties + + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilCSSInvalidValues.xhtml b/dom/smil/test/test_smilCSSInvalidValues.xhtml new file mode 100644 index 000000000..be5da6224 --- /dev/null +++ b/dom/smil/test/test_smilCSSInvalidValues.xhtml @@ -0,0 +1,59 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilCSSPaced.xhtml b/dom/smil/test/test_smilCSSPaced.xhtml new file mode 100644 index 000000000..21040dc70 --- /dev/null +++ b/dom/smil/test/test_smilCSSPaced.xhtml @@ -0,0 +1,44 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + +

+
+ + + testing 123 + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilChangeAfterFrozen.xhtml b/dom/smil/test/test_smilChangeAfterFrozen.xhtml new file mode 100644 index 000000000..91e87bc34 --- /dev/null +++ b/dom/smil/test/test_smilChangeAfterFrozen.xhtml @@ -0,0 +1,571 @@ + + + Test for SMIL when things change after an animation is frozen + + + + + +Mozilla Bug 533291 +

+ + +
+
+
+ + diff --git a/dom/smil/test/test_smilConditionalProcessing.html b/dom/smil/test/test_smilConditionalProcessing.html new file mode 100644 index 000000000..21d08adb0 --- /dev/null +++ b/dom/smil/test/test_smilConditionalProcessing.html @@ -0,0 +1,80 @@ + + + + + Test conditional processing tests applied to animations + + + + +

+
+ + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilContainerBinding.xhtml b/dom/smil/test/test_smilContainerBinding.xhtml new file mode 100644 index 000000000..1a47703bf --- /dev/null +++ b/dom/smil/test/test_smilContainerBinding.xhtml @@ -0,0 +1,101 @@ + + + + Test for adding and removing animations from a time container + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilCrossContainer.xhtml b/dom/smil/test/test_smilCrossContainer.xhtml new file mode 100644 index 000000000..2067973d6 --- /dev/null +++ b/dom/smil/test/test_smilCrossContainer.xhtml @@ -0,0 +1,132 @@ + + + + Test for moving animations between time containers + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml b/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml new file mode 100644 index 000000000..b2af10c6f --- /dev/null +++ b/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml @@ -0,0 +1,103 @@ + + + + Test for Bug 699143 + + + + + +Mozilla Bug 699143 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilExtDoc.xhtml b/dom/smil/test/test_smilExtDoc.xhtml new file mode 100644 index 000000000..772aebdaa --- /dev/null +++ b/dom/smil/test/test_smilExtDoc.xhtml @@ -0,0 +1,80 @@ + + + + Test for Bug 628888 - Animations in external document sometimes don't run + + + + +Mozilla Bug 628888 +

+
+ +
+
+
+ + diff --git a/dom/smil/test/test_smilFillMode.xhtml b/dom/smil/test/test_smilFillMode.xhtml new file mode 100644 index 000000000..b0f4b84c7 --- /dev/null +++ b/dom/smil/test/test_smilFillMode.xhtml @@ -0,0 +1,86 @@ + + + Test for SMIL fill modes + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilGetSimpleDuration.xhtml b/dom/smil/test/test_smilGetSimpleDuration.xhtml new file mode 100644 index 000000000..5c4dc33eb --- /dev/null +++ b/dom/smil/test/test_smilGetSimpleDuration.xhtml @@ -0,0 +1,86 @@ + + + Test for getSimpleDuration Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilGetStartTime.xhtml b/dom/smil/test/test_smilGetStartTime.xhtml new file mode 100644 index 000000000..9b608487c --- /dev/null +++ b/dom/smil/test/test_smilGetStartTime.xhtml @@ -0,0 +1,103 @@ + + + Test for getStartTime Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilHyperlinking.xhtml b/dom/smil/test/test_smilHyperlinking.xhtml new file mode 100644 index 000000000..542a02073 --- /dev/null +++ b/dom/smil/test/test_smilHyperlinking.xhtml @@ -0,0 +1,233 @@ + + + Test for hyperlinking + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilInvalidValues.html b/dom/smil/test/test_smilInvalidValues.html new file mode 100644 index 000000000..9cd03f49b --- /dev/null +++ b/dom/smil/test/test_smilInvalidValues.html @@ -0,0 +1,113 @@ + + + + + + Test invalid values cause the model to be updated (bug 941315) + + + + +Mozilla Bug 941315 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilKeySplines.xhtml b/dom/smil/test/test_smilKeySplines.xhtml new file mode 100644 index 000000000..a7ccb58c4 --- /dev/null +++ b/dom/smil/test/test_smilKeySplines.xhtml @@ -0,0 +1,296 @@ + + + Test for SMIL keySplines + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilKeyTimes.xhtml b/dom/smil/test/test_smilKeyTimes.xhtml new file mode 100644 index 000000000..85266ed19 --- /dev/null +++ b/dom/smil/test/test_smilKeyTimes.xhtml @@ -0,0 +1,391 @@ + + + Test for SMIL keyTimes + + + + +Mozilla Bug + 557885 +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilKeyTimesPacedMode.xhtml b/dom/smil/test/test_smilKeyTimesPacedMode.xhtml new file mode 100644 index 000000000..eff537cfd --- /dev/null +++ b/dom/smil/test/test_smilKeyTimesPacedMode.xhtml @@ -0,0 +1,123 @@ + + + Tests updated intervals + + + + +Mozilla Bug 555026 +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilMappedAttrFromBy.xhtml b/dom/smil/test/test_smilMappedAttrFromBy.xhtml new file mode 100644 index 000000000..48fea5256 --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrFromBy.xhtml @@ -0,0 +1,51 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + + +

+
+ + + + testing 123 + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilMappedAttrFromTo.xhtml b/dom/smil/test/test_smilMappedAttrFromTo.xhtml new file mode 100644 index 000000000..86e647e29 --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrFromTo.xhtml @@ -0,0 +1,79 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + + +

+
+ + + + testing 123 + + + + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilMappedAttrPaced.xhtml b/dom/smil/test/test_smilMappedAttrPaced.xhtml new file mode 100644 index 000000000..c56b3aeb7 --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrPaced.xhtml @@ -0,0 +1,46 @@ + + + Test for Animation Behavior on CSS Properties + + + + + + + + +

+
+ + + testing 123 + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilMinTiming.html b/dom/smil/test/test_smilMinTiming.html new file mode 100644 index 000000000..1a82f3c96 --- /dev/null +++ b/dom/smil/test/test_smilMinTiming.html @@ -0,0 +1,93 @@ + + + + + + Test for Bug 948245 + + + + +Mozilla Bug 948245 +

+
+ + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilRepeatDuration.html b/dom/smil/test/test_smilRepeatDuration.html new file mode 100644 index 000000000..3690a9566 --- /dev/null +++ b/dom/smil/test/test_smilRepeatDuration.html @@ -0,0 +1,139 @@ + + + + + + Test for repeat duration calculation (Bug 948245) + + + + +Mozilla Bug 948245 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilRepeatTiming.xhtml b/dom/smil/test/test_smilRepeatTiming.xhtml new file mode 100644 index 000000000..74a9c17af --- /dev/null +++ b/dom/smil/test/test_smilRepeatTiming.xhtml @@ -0,0 +1,96 @@ + + + + Test repeat timing + + + + +Mozilla Bug + 485157 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilReset.xhtml b/dom/smil/test/test_smilReset.xhtml new file mode 100644 index 000000000..272d5cc0a --- /dev/null +++ b/dom/smil/test/test_smilReset.xhtml @@ -0,0 +1,82 @@ + + + Tests for SMIL Reset Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilRestart.xhtml b/dom/smil/test/test_smilRestart.xhtml new file mode 100644 index 000000000..a4eab2e8c --- /dev/null +++ b/dom/smil/test/test_smilRestart.xhtml @@ -0,0 +1,102 @@ + + + Test for SMIL Restart Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilSetCurrentTime.xhtml b/dom/smil/test/test_smilSetCurrentTime.xhtml new file mode 100644 index 000000000..36f64a49f --- /dev/null +++ b/dom/smil/test/test_smilSetCurrentTime.xhtml @@ -0,0 +1,76 @@ + + + Test for setCurrentTime Behavior + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilSync.xhtml b/dom/smil/test/test_smilSync.xhtml new file mode 100644 index 000000000..0db9dd5a2 --- /dev/null +++ b/dom/smil/test/test_smilSync.xhtml @@ -0,0 +1,255 @@ + + + Test for SMIL sync behaviour + + + + +

+
+ + + + + + + + + + + + + + + + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilSyncTransform.xhtml b/dom/smil/test/test_smilSyncTransform.xhtml new file mode 100644 index 000000000..65debc97c --- /dev/null +++ b/dom/smil/test/test_smilSyncTransform.xhtml @@ -0,0 +1,66 @@ + + + Test for SMIL sync behaviour for transform types + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilSyncbaseTarget.xhtml b/dom/smil/test/test_smilSyncbaseTarget.xhtml new file mode 100644 index 000000000..979c15391 --- /dev/null +++ b/dom/smil/test/test_smilSyncbaseTarget.xhtml @@ -0,0 +1,180 @@ + + + + Test for syncbase targetting + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilTextZoom.xhtml b/dom/smil/test/test_smilTextZoom.xhtml new file mode 100644 index 000000000..814b81f6e --- /dev/null +++ b/dom/smil/test/test_smilTextZoom.xhtml @@ -0,0 +1,89 @@ + + + Test for SMIL Animation Behavior with textZoom + + + + + +

+
+ + + abc + + + + + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilTimeEvents.xhtml b/dom/smil/test/test_smilTimeEvents.xhtml new file mode 100644 index 000000000..bf6924ddb --- /dev/null +++ b/dom/smil/test/test_smilTimeEvents.xhtml @@ -0,0 +1,337 @@ + + + + Test TimeEvents dispatching + + + + +Mozilla Bug + 572270 +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilTiming.xhtml b/dom/smil/test/test_smilTiming.xhtml new file mode 100644 index 000000000..cab1d4080 --- /dev/null +++ b/dom/smil/test/test_smilTiming.xhtml @@ -0,0 +1,291 @@ + + + Test for SMIL timing + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilTimingZeroIntervals.xhtml b/dom/smil/test/test_smilTimingZeroIntervals.xhtml new file mode 100644 index 000000000..610cb5798 --- /dev/null +++ b/dom/smil/test/test_smilTimingZeroIntervals.xhtml @@ -0,0 +1,285 @@ + + + Test for SMIL timing with zero-duration intervals + + + + +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilUpdatedInterval.xhtml b/dom/smil/test/test_smilUpdatedInterval.xhtml new file mode 100644 index 000000000..26b793dc6 --- /dev/null +++ b/dom/smil/test/test_smilUpdatedInterval.xhtml @@ -0,0 +1,64 @@ + + + Tests updated intervals + + + + +

+ +
+
+
+ + diff --git a/dom/smil/test/test_smilValues.xhtml b/dom/smil/test/test_smilValues.xhtml new file mode 100644 index 000000000..d2bce96a3 --- /dev/null +++ b/dom/smil/test/test_smilValues.xhtml @@ -0,0 +1,171 @@ + + + Test for SMIL values + + + + +Mozilla Bug + 474742 +

+
+ + + +
+
+
+
+ + diff --git a/dom/smil/test/test_smilXHR.xhtml b/dom/smil/test/test_smilXHR.xhtml new file mode 100644 index 000000000..d5202090e --- /dev/null +++ b/dom/smil/test/test_smilXHR.xhtml @@ -0,0 +1,88 @@ + + + Test for SMIL Behavior in Data Documents + + + + + +Mozilla Bug 529387 +

+ +
+
+
+ + -- cgit v1.2.3