summaryrefslogtreecommitdiffstats
path: root/dom/smil
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /dom/smil
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/smil')
-rw-r--r--dom/smil/SMILBoolType.cpp83
-rw-r--r--dom/smil/SMILBoolType.h50
-rw-r--r--dom/smil/SMILEnumType.cpp83
-rw-r--r--dom/smil/SMILEnumType.h51
-rw-r--r--dom/smil/SMILIntegerType.cpp100
-rw-r--r--dom/smil/SMILIntegerType.h46
-rw-r--r--dom/smil/SMILStringType.cpp91
-rw-r--r--dom/smil/SMILStringType.h51
-rw-r--r--dom/smil/TimeEvent.cpp79
-rw-r--r--dom/smil/TimeEvent.h71
-rw-r--r--dom/smil/crashtests/1010681-1.svg23
-rw-r--r--dom/smil/crashtests/483584-1.svg8
-rw-r--r--dom/smil/crashtests/483584-2.svg133
-rw-r--r--dom/smil/crashtests/523188-1.svg15
-rw-r--r--dom/smil/crashtests/525099-1.svg7
-rw-r--r--dom/smil/crashtests/526536-1.svg19
-rw-r--r--dom/smil/crashtests/526875-1.svg4
-rw-r--r--dom/smil/crashtests/526875-2.svg4
-rw-r--r--dom/smil/crashtests/529387-1-helper.svg5
-rw-r--r--dom/smil/crashtests/529387-1.xhtml7
-rw-r--r--dom/smil/crashtests/531550-1.svg3
-rw-r--r--dom/smil/crashtests/537157-1.svg11
-rw-r--r--dom/smil/crashtests/541297-1.svg22
-rw-r--r--dom/smil/crashtests/547333-1.svg22
-rw-r--r--dom/smil/crashtests/548899-1.svg14
-rw-r--r--dom/smil/crashtests/551620-1.svg21
-rw-r--r--dom/smil/crashtests/554141-1.svg12
-rw-r--r--dom/smil/crashtests/554202-1.svg31
-rw-r--r--dom/smil/crashtests/554202-2.svg19
-rw-r--r--dom/smil/crashtests/555026-1.svg25
-rw-r--r--dom/smil/crashtests/556841-1.svg16
-rw-r--r--dom/smil/crashtests/572938-1.svg12
-rw-r--r--dom/smil/crashtests/572938-2.svg22
-rw-r--r--dom/smil/crashtests/572938-3.svg10
-rw-r--r--dom/smil/crashtests/572938-4.svg10
-rw-r--r--dom/smil/crashtests/588287-1.svg24
-rw-r--r--dom/smil/crashtests/588287-2.svg26
-rw-r--r--dom/smil/crashtests/590425-1.html24
-rw-r--r--dom/smil/crashtests/592477-1.xhtml26
-rw-r--r--dom/smil/crashtests/594653-1.svg26
-rw-r--r--dom/smil/crashtests/596796-1.svg15
-rw-r--r--dom/smil/crashtests/605345-1.svg25
-rw-r--r--dom/smil/crashtests/606101-1.svg23
-rw-r--r--dom/smil/crashtests/608295-1.html18
-rw-r--r--dom/smil/crashtests/608549-1.svg29
-rw-r--r--dom/smil/crashtests/611927-1.svg4
-rw-r--r--dom/smil/crashtests/615002-1.svg16
-rw-r--r--dom/smil/crashtests/615872-1.svg21
-rw-r--r--dom/smil/crashtests/641388-1.html98
-rw-r--r--dom/smil/crashtests/641388-2.html79
-rw-r--r--dom/smil/crashtests/650732-1.svg46
-rw-r--r--dom/smil/crashtests/665334-1.svg13
-rw-r--r--dom/smil/crashtests/669225-1.svg21
-rw-r--r--dom/smil/crashtests/669225-2.svg21
-rw-r--r--dom/smil/crashtests/670313-1.svg20
-rw-r--r--dom/smil/crashtests/678822-1.svg3
-rw-r--r--dom/smil/crashtests/678847-1.svg3
-rw-r--r--dom/smil/crashtests/678938-1.svg11
-rw-r--r--dom/smil/crashtests/690994-1.svg17
-rw-r--r--dom/smil/crashtests/691337-1.svg8
-rw-r--r--dom/smil/crashtests/691337-2.svg11
-rw-r--r--dom/smil/crashtests/697640-1.svg3
-rw-r--r--dom/smil/crashtests/699325-1.svg5
-rw-r--r--dom/smil/crashtests/709907-1.svg3
-rw-r--r--dom/smil/crashtests/720103-1.svg4
-rw-r--r--dom/smil/crashtests/crashtests.list54
-rw-r--r--dom/smil/moz.build72
-rw-r--r--dom/smil/nsISMILAttr.h98
-rw-r--r--dom/smil/nsISMILType.h214
-rw-r--r--dom/smil/nsSMILAnimationController.cpp795
-rw-r--r--dom/smil/nsSMILAnimationController.h211
-rw-r--r--dom/smil/nsSMILAnimationFunction.cpp1070
-rw-r--r--dom/smil/nsSMILAnimationFunction.h458
-rw-r--r--dom/smil/nsSMILCSSProperty.cpp275
-rw-r--r--dom/smil/nsSMILCSSProperty.h67
-rw-r--r--dom/smil/nsSMILCSSValueType.cpp447
-rw-r--r--dom/smil/nsSMILCSSValueType.h116
-rw-r--r--dom/smil/nsSMILCompositor.cpp204
-rw-r--r--dom/smil/nsSMILCompositor.h107
-rw-r--r--dom/smil/nsSMILCompositorTable.h23
-rw-r--r--dom/smil/nsSMILFloatType.cpp92
-rw-r--r--dom/smil/nsSMILFloatType.h47
-rw-r--r--dom/smil/nsSMILInstanceTime.cpp212
-rw-r--r--dom/smil/nsSMILInstanceTime.h166
-rw-r--r--dom/smil/nsSMILInterval.cpp170
-rw-r--r--dom/smil/nsSMILInterval.h86
-rw-r--r--dom/smil/nsSMILKeySpline.cpp151
-rw-r--r--dom/smil/nsSMILKeySpline.h122
-rw-r--r--dom/smil/nsSMILMappedAttribute.cpp150
-rw-r--r--dom/smil/nsSMILMappedAttribute.h56
-rw-r--r--dom/smil/nsSMILMilestone.h79
-rw-r--r--dom/smil/nsSMILNullType.cpp56
-rw-r--r--dom/smil/nsSMILNullType.h50
-rw-r--r--dom/smil/nsSMILParserUtils.cpp730
-rw-r--r--dom/smil/nsSMILParserUtils.h89
-rw-r--r--dom/smil/nsSMILRepeatCount.cpp10
-rw-r--r--dom/smil/nsSMILRepeatCount.h62
-rw-r--r--dom/smil/nsSMILSetAnimationFunction.cpp101
-rw-r--r--dom/smil/nsSMILSetAnimationFunction.h68
-rw-r--r--dom/smil/nsSMILTargetIdentifier.h86
-rw-r--r--dom/smil/nsSMILTimeContainer.cpp339
-rw-r--r--dom/smil/nsSMILTimeContainer.h298
-rw-r--r--dom/smil/nsSMILTimeValue.cpp41
-rw-r--r--dom/smil/nsSMILTimeValue.h135
-rw-r--r--dom/smil/nsSMILTimeValueSpec.cpp536
-rw-r--r--dom/smil/nsSMILTimeValueSpec.h131
-rw-r--r--dom/smil/nsSMILTimeValueSpecParams.h68
-rw-r--r--dom/smil/nsSMILTimedElement.cpp2444
-rw-r--r--dom/smil/nsSMILTimedElement.h673
-rw-r--r--dom/smil/nsSMILTypes.h26
-rw-r--r--dom/smil/nsSMILValue.cpp162
-rw-r--r--dom/smil/nsSMILValue.h81
-rw-r--r--dom/smil/test/db_smilAnimateMotion.js253
-rw-r--r--dom/smil/test/db_smilCSSFromBy.js166
-rw-r--r--dom/smil/test/db_smilCSSFromTo.js483
-rw-r--r--dom/smil/test/db_smilCSSPaced.js321
-rw-r--r--dom/smil/test/db_smilCSSPropertyList.js93
-rw-r--r--dom/smil/test/db_smilMappedAttrList.js131
-rw-r--r--dom/smil/test/mochitest.ini60
-rw-r--r--dom/smil/test/smilAnimateMotionValueLists.js128
-rw-r--r--dom/smil/test/smilExtDoc_helper.svg7
-rw-r--r--dom/smil/test/smilTestUtils.js858
-rw-r--r--dom/smil/test/smilXHR_helper.svg8
-rw-r--r--dom/smil/test/test_smilAccessKey.xhtml362
-rw-r--r--dom/smil/test/test_smilAnimateMotion.xhtml51
-rw-r--r--dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml176
-rw-r--r--dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml215
-rw-r--r--dom/smil/test/test_smilBackwardsSeeking.xhtml191
-rw-r--r--dom/smil/test/test_smilCSSFontStretchRelative.xhtml102
-rw-r--r--dom/smil/test/test_smilCSSFromBy.xhtml49
-rw-r--r--dom/smil/test/test_smilCSSFromTo.xhtml76
-rw-r--r--dom/smil/test/test_smilCSSInherit.xhtml85
-rw-r--r--dom/smil/test/test_smilCSSInvalidValues.xhtml59
-rw-r--r--dom/smil/test/test_smilCSSPaced.xhtml44
-rw-r--r--dom/smil/test/test_smilChangeAfterFrozen.xhtml571
-rw-r--r--dom/smil/test/test_smilConditionalProcessing.html80
-rw-r--r--dom/smil/test/test_smilContainerBinding.xhtml101
-rw-r--r--dom/smil/test/test_smilCrossContainer.xhtml132
-rw-r--r--dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml103
-rw-r--r--dom/smil/test/test_smilExtDoc.xhtml80
-rw-r--r--dom/smil/test/test_smilFillMode.xhtml86
-rw-r--r--dom/smil/test/test_smilGetSimpleDuration.xhtml86
-rw-r--r--dom/smil/test/test_smilGetStartTime.xhtml103
-rw-r--r--dom/smil/test/test_smilHyperlinking.xhtml233
-rw-r--r--dom/smil/test/test_smilInvalidValues.html113
-rw-r--r--dom/smil/test/test_smilKeySplines.xhtml296
-rw-r--r--dom/smil/test/test_smilKeyTimes.xhtml391
-rw-r--r--dom/smil/test/test_smilKeyTimesPacedMode.xhtml123
-rw-r--r--dom/smil/test/test_smilMappedAttrFromBy.xhtml51
-rw-r--r--dom/smil/test/test_smilMappedAttrFromTo.xhtml79
-rw-r--r--dom/smil/test/test_smilMappedAttrPaced.xhtml46
-rw-r--r--dom/smil/test/test_smilMinTiming.html93
-rw-r--r--dom/smil/test/test_smilRepeatDuration.html139
-rw-r--r--dom/smil/test/test_smilRepeatTiming.xhtml96
-rw-r--r--dom/smil/test/test_smilReset.xhtml82
-rw-r--r--dom/smil/test/test_smilRestart.xhtml102
-rw-r--r--dom/smil/test/test_smilSetCurrentTime.xhtml76
-rw-r--r--dom/smil/test/test_smilSync.xhtml255
-rw-r--r--dom/smil/test/test_smilSyncTransform.xhtml66
-rw-r--r--dom/smil/test/test_smilSyncbaseTarget.xhtml180
-rw-r--r--dom/smil/test/test_smilTextZoom.xhtml89
-rw-r--r--dom/smil/test/test_smilTimeEvents.xhtml337
-rw-r--r--dom/smil/test/test_smilTiming.xhtml291
-rw-r--r--dom/smil/test/test_smilTimingZeroIntervals.xhtml285
-rw-r--r--dom/smil/test/test_smilUpdatedInterval.xhtml64
-rw-r--r--dom/smil/test/test_smilValues.xhtml171
-rw-r--r--dom/smil/test/test_smilXHR.xhtml88
167 files changed, 22557 insertions, 0 deletions
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 <math.h>
+
+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 <math.h>
+
+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 <math.h>
+
+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<nsAString*>(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<const nsAString*>(aSrc.mU.mPtr);
+ nsAString* dst = static_cast<nsAString*>(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<const nsAString*>(aLeft.mU.mPtr);
+ const nsAString* rightString =
+ static_cast<nsAString*>(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<nsIDocShell> 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<TimeEvent>
+NS_NewDOMTimeEvent(EventTarget* aOwner,
+ nsPresContext* aPresContext,
+ InternalSMILTimeEvent* aEvent)
+{
+ RefPtr<TimeEvent> 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<JSObject*> 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<nsPIDOMWindowOuter> mView;
+ int32_t mDetail;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+already_AddRefed<mozilla::dom::TimeEvent>
+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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+ class="reftest-wait">
+<script>
+<![CDATA[
+
+function boom()
+{
+ var animate =
+ document.createElementNS("http://www.w3.org/2000/svg", "animate");
+ animate.setAttribute("dur", "2s");
+ document.documentElement.appendChild(animate);
+ animate.targetElement;
+ animate.requiredFeatures.insertItemBefore(0, 0);
+ document.documentElement.setCurrentTime(4);
+ document.documentElement.setCurrentTime(0);
+ document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("load", boom, false);
+
+]]>
+</script></svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <g id="s">
+ <circle/>
+ <animateTransform attributeName="transform"/>
+ </g>
+ <use xlink:href="#s"/>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- =====================================================================-->
+<!-- animate-elem-30-t.svg -->
+<!-- -->
+<!-- Tests various types of animations on referenced elements. -->
+<!-- -->
+<!-- Author : Ola Andersson, 22-Sep-2003 -->
+<!--======================================================================-->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="tiny" id="svg-root" width="480" height="360" viewBox="0 0 480 360" onload="go()" class="reftest-wait">
+ <script>
+ function go() {
+ var svg = document.documentElement;
+ svg.pauseAnimations();
+
+ // Note: Animations in this testcase have begin="100" dur="3".
+
+ // Jump to partway through animation...
+ svg.setCurrentTime(102);
+
+ // ...and then (if we didn't hang) jump back to a pre-animation time.
+ svg.setCurrentTime(50);
+
+ // Signal that the test is complete:
+ svg.removeAttribute("class");
+ }
+ </script>
+ <g transform="translate(20) scale(1.3)">
+ <!-- SILHOUETTES-->
+ <path d="M210 40 C210 40 210 100 170 190" fill="none" stroke="#b4b4b4"/>
+ <path d="M 171 188 l 10 -10 l -10 -4 z" fill="#b4b4b4" stroke="none"/>
+ <polyline fill="none" stroke="#b4b4b4" stroke-width="2" points="200,20 200,40 220,40 220,60"/>
+ <polyline transform="rotate(15)" fill="none" stroke="#b4b4b4" stroke-width="9" points="200,120 200,140 220,140 220,160"/>
+
+ <line x1="40" y1="50" x2="20" y2="10" stroke="#b4b4b4" stroke-width="3"/>
+ <line x1="160" y1="50" x2="80" y2="10" stroke="#b4b4b4" stroke-width="3"/>
+ <line x1="30" y1="30" x2="120" y2="30" stroke="#b4b4b4"/>
+ <path d="M 120 30 l -10 3 l 0 -6 z" fill="#b4b4b4" stroke="none"/>
+
+ <line x1="70" y1="70" x2="145" y2="70" stroke="#b4b4b4"/>
+ <rect x="10" y="60" width="60" height="20" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/>
+ <rect x="145" y="60" width="30" height="40" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/>
+ <path d="M 145 70 l -10 3 l 0 -6 z" fill="#b4b4b4" stroke="none"/>
+
+ <circle cx="30" cy="100" r="10" fill="#b4b4b4" stroke="#b4b4b4"/>
+ <circle cx="100" cy="195" r="15" fill="#b4b4b4" stroke="#b4b4b4"/>
+ <line x1="37" y1="107" x2="89" y2="184" stroke="#b4b4b4"/>
+ <path d="M 90 185 l -3 -12 l -6 5 z" fill="#b4b4b4" stroke="none"/>
+
+ <rect x="300" y="10" width="20" height="20" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/>
+ <rect x="300" y="170" width="20" height="40" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/>
+ <line x1="310" y1="30" x2="310" y2="170" stroke="#b4b4b4"/>
+ <path d="M 310 170 l -3 -10 l 6 0 z" fill="#b4b4b4" stroke="none"/>
+
+ <rect x="230" y="7.5" width="40" height="7.5" fill="#b4b4b4" stroke="none"/>
+ <rect x="230" y="170" width="40" height="30" fill="#b4b4b4" stroke="none"/>
+ <line x1="250" y1="10" x2="250" y2="170" stroke="#b4b4b4"/>
+ <path d="M 250 170 l -3 -10 l 6 0 z" fill="#b4b4b4" stroke="none"/>
+ <!-- END OF SILHOUETTES-->
+
+ <!-- DEFS-->
+ <defs>
+ <line id="lineID" x1="30" y1="50" x2="10" y2="10" stroke="rgb(16, 93, 140)" stroke-width="3">
+ <animate attributeName="x1" from="30" to="90" begin="100" dur="3" fill="freeze"/>
+ </line>
+ </defs>
+
+ <defs>
+ <rect id="rectID" x="10" y="60" width="60" height="20" fill="blue" stroke="black" stroke-width="2">
+ <animateColor attributeName="fill" from="white" to="rgb(16, 93, 140)" begin="100" dur="3" fill="freeze"/>
+ <animate attributeName="height" from="20" to="40" begin="100" dur="3" fill="freeze"/>
+ </rect>
+ </defs>
+
+ <defs>
+ <circle id="circleID" cx="20" cy="100" r="10" fill="rgb(16, 93, 140)" stroke="black" transform="">
+ <animate attributeName="cy" from="100" to="130" begin="100" dur="3" fill="freeze"/>
+ <animateTransform attributeName="transform" type="scale" from="1" to="1.5" additive="sum" begin="100" dur="3" fill="freeze"/>
+ </circle>
+ </defs>
+
+ <defs>
+ <polyline id="polylineID" fill="none" stroke="rgb(16, 93, 140)" stroke-width="2" points="200,20 200,40 220,40 220,60">
+ <animateMotion path="M 0 0 l 0 100" begin="100" dur="3" fill="freeze"/>
+ <animate attributeName="stroke-width" from="2" to="9" begin="100" dur="3" fill="freeze"/>
+ </polyline>
+ </defs>
+
+ <defs>
+ <polygon id="polygonID" fill="green" stroke="black" points="240,20 240,40 260,40 260,20" stroke-width="2">
+ <animate attributeName="fill" from="white" to="rgb(16, 93, 140)" begin="100" dur="3" fill="freeze"/>
+ </polygon>
+ </defs>
+
+ <defs>
+ <image id="imageID" x="230" y="20" width="40" height="80" xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEASABIAAD/4RX+RXhpZgAASUkqAAgAAAAJAA8BAgAGAAAAegAAABABAgAXAAAAgAAAABIBAwABAAAAAQAAABoBBQABAAAAoAAAABsBBQABAAAAqAAAACgBAwABAAAAAgAAADIBAgAUAAAAsAAAABMCAwABAAAAAQAAAGmHBAABAAAAxAAAAGYFAABDYW5vbgBDYW5vbiBESUdJVEFMIElYVVMgMzAwAAAAAAAAAAAAALQAAAABAAAAtAAAAAEAAAAyMDAyOjAxOjE1IDA0OjQyOjU4ABsAmoIFAAEAAABWAwAAnYIFAAEAAABeAwAAAJAHAAQAAAAwMjEwA5ACABQAAAAOAgAABJACABQAAAAiAgAAAZEHAAQAAAABAgMAApEFAAEAAAA+AwAAAZIKAAEAAABGAwAAApIFAAEAAABOAwAABJIKAAEAAABmAwAABZIFAAEAAABuAwAABpIFAAEAAAB2AwAAB5IDAAEAAAAFAAAACZIDAAEAAAAAAAAACpIFAAEAAAB+AwAAfJIHAJoBAACGAwAAhpIHAAgBAAA2AgAAAKAHAAQAAAAwMTAwAaADAAEAAAABAAAAAqADAAEAAABABgAAA6ADAAEAAACwBAAABaAEAAEAAAAwBQAADqIFAAEAAAAgBQAAD6IFAAEAAAAoBQAAEKIDAAEAAAACAAAAF6IDAAEAAAACAAAAAKMHAAEAAAADAAAAAAAAADIwMDI6MDE6MTUgMDQ6NDI6NTgAMjAwMjowMToxNSAwNDo0Mjo1OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAQAAAPUAAAAgAAAA1QAAACAAAAABAAAAyAAAAGQAAAAKAAAAAAAAAAMAAACs3QIAAAABAP//AADoAwAAMAEAACAAAAAMAAEAAwAmAAAAHAQAAAIAAwAEAAAAaAQAAAMAAwAEAAAAcAQAAAQAAwAaAAAAeAQAAAAAAwAGAAAArAQAAAAAAwAEAAAAuAQAAAYAAgAgAAAAwAQAAAcAAgAYAAAA4AQAAAgABAABAAAAcVYQAAkAAgAgAAAA+AQAABAABAABAAAAAAAEAQ0AAwAEAAAAGAUAAAAAAABMAAIAAAADAAEAAAAAAAQAAAABAAAAAAAAAAAAAAAAAAAAAwABAAEwAAD/////BgKtACAAdADVAP//AAAAAAAAAAAAAP//AABABkAGAgAwAdMAngAAAAAAAAAAADQAAACPAD8B1QD1AAAAAAAAAAEAAwAAAAAAAAAHMAAAAAAAAAAA//8AANUA+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElNRzpESUdJVEFMIElYVVMgMzAwIEpQRUcAAAAAAAAARmlybXdhcmUgVmVyc2lvbiAxLjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAPQA9AD0AABqGADOAAAAgE8SAJsAAAAEAAEAAgAEAAAAUjk4AAIABwAEAAAAMDEwMAEQAwABAAAAQAYAAAIQAwABAAAAsAQAAAAAAAAGAAMBAwABAAAABgAAABoBBQABAAAAtAUAABsBBQABAAAAvAUAACgBAwABAAAAAgAAAAECBAABAAAA9AUAAAICBAABAAAA7g8AAAAAAAC0AAAAAQAAALQAAAABAAAA//////////////////////////////////////////////////9//////////////9j/2wCEAAkGBggGBQkIBwgKCQkLDRYPDQwMDRwTFRAWIR0jIiEcIB8kKTQsJCcxJx4fLT0tMTY3Ojo6Iio/RD44QjM3OTYBCQkJDAoMFAwMFA8KCgoPGhoKChoaTxoaGhoaT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT//AABEIAHgAoAMBIQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AOxApcV7J5goUngDNIVweaBC80oYjvRYLseszAY60FjI3c1HLZ3L5r6DjG46jFRFjTVmJtoA5qW3j3vuIBAokuVNgnzOxpKVA5xUNyY3TB4Fc0b3Oh2sZbqAxx0pMV2pnG9xNtJincA20hWmBMBT1XJrJuxaLf7pU4wKqScuayp3vqaTtbQABjmnRJunQdiRVtuz8kQlsa0kUUg+ZFP4VVeyUNuiJB9DXn060oabx6o7Z0oy12l3I5pMDawwcVTIrupbX6PY46u9uq3ExT0kZBxWrV9DNOw4zue9MZi3U0lFIbm2NwKTFUIMUmKYgxSYoAmAFP2+9Z3NLCbaTbzRcmw9baR13KpIp8ETLcpuUjnvWcqsbNXXMk9DSNOV07aX3NInFNPTmvLO8iljWVcN19az2hZZNmMkniuzC1LXg9t0c2Ihe0lv1JZrGWEZIBX1FV8V1U6iqK6+aOacHB2fyYYorQkMUmKYgxSYoEGKTFAyYCnYrM0FCM33VJ+go8mQfwN+VS5xWjaTDlk9kzSRBHGq+gpoO6ZB6mvLk7tvuz0ErK3YlkIQVAz0hkZamhlWRXIztNCdtVuDV9zRDBlBGCCKz7uyO8NCuQeoHatqFTkld/C9zOrDnj5rYYunSn7xVfqaf/Zn/TTn/drpli4p6Lm89jnjhn1dvIo3TRWt5HbGQNNICwUdQB3NGK3pVPaR5tvIyqQ5HbfzDFJitDMMUYoAlxSgVmaGlAAkCjvjNKeTXlzd5N92ehFWSXZCHmoyQjBj2qGUMkn3GojJQA0yUKGf7ozQBbtd6x4fgZ4qxmhMGGcCsjxP4ktvDGiS39yQSPljjzy79hTEcN8P7y51mbUNd1KQGSZxGrHgDP8ACPyFdtivRw1vZ6dHqcNe/P6rQcIXPRGP4UxsLJ5bEB8Z298etbc8b2ur9jLkklezt3DFGKsRLQKzLNFJULiPeofGduecfSiR/wB4qD6mvIb/ADPSQuPWqt+3lxKfVgKT2AqmSk3E9KALUFkW+aTgelWtqhhGgx647Cj+kBI/3Pl7UwOCMijqBICGXNcj448Ax+MfLkN/NbyQqRGmA0fPcj+tMCPwz8PRpWnWcN/ePKYG83yovljL+p7nH4V1sjQWcLSyFIo0GWduAB9apSaVk7J7onlTd+q2Z5n4x+MKWxa08PKJW+610w4B/wBkd/rWr4Es7pdE/tDUpHlvb7EsjyHJ2/wj8j+tb4WN537IyxDtC3dnSYpMV6Jwkt8sktm6wSLFMynY5G7afpXkl3Y61qVyY7zVrkxkncN52kewBx+lediJuKWrSfQ76EVJvRO3Ut6zo0+p3Ed19rc3MUYRC3QADjGOhrofCGvz6bpPk61cyTXab9ryMWLDjA3f56VxKVzqcbGA/wAWdZtrly8CGLcdvm2xAx9Q2f0rc0P4lDxPKbae2t4XjXzN0UrENj/ZZRj86p/CQtzoZryKCJpZpFSNeSxPArk7z4iTC/UaMtkYUJ3SXT43n0AH86Fq7feGxq6Z8VbOWdLfVbY20zEKrwSCaMn8OR+VXbn4laFp9xLDJJcSzLkuIoSQvtk4FNiSuZU3xhtCT9i0i7nHrJIkf9TWx4Y8YxeIIpC8Is5g3+pMofH48UPa4Lsak3iTTbGyuJ7m6RI7Ztsp67T9PxqDTPG2ia7c/ZdMvBNMRu27COO55FF9ANHV9Zs9D097u+lEcSD8WPoBXhPjf4g6j4puWt4w9tYqflhBwW929aa19AMTw9aW93rlnDqEghtjMDIzdMelfREQR4lMJUxkfKV6Y9q7sK1Z9zkxKd12HbKPLrsucyRxsuuXeq25SOUKu8gPESpOPftWMblrWcLJueI/dmHI/E/1rw61R1Ja9NkevSgoR069TQVy6EoVbjiqsy3ZiO+GFXGNoEpIP47axNSgEvPNEF2trI0udqtKRkAc8bKbaaVGkpntbK383JV2W4ZcH0+7zVXttdE2vvZly4tLq8h8q5t4pIyeVN45H/oFVR4cjwB/Zdpjtm6f/wCJpKVurHy+SLFto2m7leK3iEqHBKMTtYe9OGmafcM8hgjkdnIdjzn1pOTGoortoMIkbytNsiueCzkE/wDjpp8FobWbZb2llE57JMQf/QafN5sXL5IfPo8l4jR3SoiH5iEkLZYdCQRin2tzc6FeR3S+XIkYwX+6Rn17VUZW03RMoX12Zj+JdQufEOprcXOr26LH/q4lOQtZc2keYd0uqQMQOpQZquZ9mRZdyGDRx5+WvbfAPH3Rmuy0XXLzw/A/2cG7gJ4QPuVfy71pTquDvYmcFNWbNmx+JFoqyfb4pyxclfLjXCr2H3ucetaVt4+0W4OPOkiP/TRMfrXbHFQe90zklhpLazRyMNxZ21v5K288aHPApIb3TbW0S2iSZUUbQCCePQ15soy6pps7ozj0asgtLnTbIsIXmVDzsIOF+lWDqliwO6duTkEjpUtd0ylLzRQ1KPS9TMZkvZImiyFaNtp5x/hViyfT9Ph8q3uiUzn5jk/nQ2rWBb3Lkd/bYybhT7U4X9sDj7SnHvU6FXZS8i3NvPHHqAjMzM+4EAqSe1Q6fNp+kWotZNTEzK2Q0jYNVvsTe2uyJW16yQEC+gbHvTtN8S6PaXsk1xHFdyOVKlwMIADkA571SjZ9/IHJNb2H3niawu7xpYpEgjzxGqggD65rOnbTjayQx3yqkyjOV3EHGOxH5UW12Fdd9jMtvDmhxXsdw140qoM+U0eVc+/tTNV8PWd/J5kV4kSseALfvgZ6DNac2mxHKu5WPgm23AW+qOwZQDut2Xn04zxnFT2ng9LEiSLWbm3nGdzRWz4Ht2zS5vIfL5mja2chB+33kF1nAz5Tow/HFOm0mFoSI7xAwPyoyEjHfnH9PWpv6jS80dK8cJGdwz9aiFvHnOBXqSVzzouwvkJnBVc/SnizQ/8ALMflXPKKNVJgbCIjmMflUf2C3brEpH+7kVm4ItSGtpdqRzBHj12CoG0mzbAMMeO3FR7MrmFGkWfCmEfhUM3hyykk3FXBPo5oVJBzsrt4QsHck+aPo/8A9aoz4K08E/vJP0P9Kr2fYnnI5PBViQQJZF/Af4VA3gi1B4nbp/cWj2TH7Qhk8GQAACZuneMCq0vhDYMpMufdf/r1SovuJ1UVz4WlLcNH9cH/ABqQeDLhjlZIPx3CmsPJ9SXWS6Cnwhfx/ceL8JmH9KP+Ea1ZD8s34i5Yf0p/Vp+TBYiPmd35eOgIoAJHBH511M50O29C4U47mnhQykBiT6BqxZqhwJTG4N9OtSrtY98+/Ws5eRcfMCgJzj2yKjaMZ64x6VCZTQ5VULyfxNIGRgR5gb2NUri0HeUj5GUcdhjpQzIBhlyvrkD+tUtfInReY37RbO2zcPTOajaKFXz5hI+uaaUkDcWRzRIVzge2BVaXYCBzitY/kZSsvmQmNQc5qSEAg7cH29K0iRIcSGGGcj2yKcANuA/Hck5NWQSzzKR8u/B6hRinIwAA+bH8qybNUSD/AH2A9MnFPDYXA5H51k0aIRkDMA0av6e1SCFduCij6VnJtFJJiGCI9ET64puVU4wc+oBqE2y7JApTdtXeD64OKcx6DJB9s1RIi5ZuCAMdQc0hWRELABiO3JNVdE2ZD+9UBinPcBj/AIUiXEj5DAoMd8mrsmTdr5jHBA+VcH1AzUTKyj69+laRsQ7kZXPynDe2KdHDGP8Alnj8K0RmxzBAOij0HSnxIsikMiqPUNVCHS28RxvJJ+tMEaRP8oOPUtmsWaIsIePf2pySgdT9NwrFo1TFNxs5YoR2waYssLO3mJHk9e+aVn0uO662BhCWyhUevz8flTiM/wCrOf8AgVRfv99i15fdcbmVcMUZiB0UgioJL64QkfZZAPUNVxSe1iJSa6MbHfSsQVtnXuS3FTfanYjfGoxxgfMavl8yObyHl02kLGcn/pkaYJJBn5MA+vGaSXdjv2RCbmYEqVXPs1NeUsvJXPrWkUlsQ5N76EQjycb2+meKsIDj1x6CtIoiV+o/eQvfHuKA/wAvYfhVkjC0gHVQfXFRkXDYO9GI6DFYNmthn+mZz8uP9lQamjmuVA/csffgVDsylddCdLhgp3oE79aie5lxhFWQexJqeW3Urm8vkVvt8gfbIoA/3CDVhJVxlCg+oNJx82ClforoZNdXce0pFGy/3gc5pq307H96kZB7KDQoIHN9kSf2gVOFXr2KkU7+1sAB1z9Oark8xe0EXU4n6RsD9KSd4WQFhz24zTUXHzE5KXkQOY9uMNjsNtIEj2/xfTbjNap+RnZdxu1edq7f6frT0hdlyJG/4C55ql9xLEMjxcBps/gc09LknO/cfZkH+NNaAOUTyjLsY19AaUzJHncWPrmsWro1TsC38J6tge4NONxBMAFc59FJrFprbU1Ti9HoD2sLJhmbHo3SmJZwA5Bj46dqqM7kygiVJkiB+YnPbPFMa9jfcFwSPek43dxqSSsNa7mUbvLVB7tSHUHWLcYmHtkHNCh5sXP5EB1p0TAtHx6g4/lU0OqQyLlrd93q4z+tVZ9G0LnXVCyXdw5/cBMf7uaga+uQwLxIT6gYqlHzZLl5KwNcs7ZljOT6UmYznkp+JFaIhjMRqM+ac/72aljDsR+/z9GqkTYk8uRTnex+rVIszIMfOSfamB//2apR82S5eSsDXLO2ZYzk+lJmM//Y/+0O2lBob3Rvc2hvcCAzLjAAOEJJTQPtClJlc29sdXRpb24AAAAAEABIAAAAAQACAEgAAAABAAI4QklNBA0YRlggR2xvYmFsIExpZ2h0aW5nIEFuZ2xlAAAAAAQAAAAeOEJJTQQZEkZYIEdsb2JhbCBBbHRpdHVkZQAAAAAEAAAAHjhCSU0D8wtQcmludCBGbGFncwAAAAkAAAAAAAAAAAEAOEJJTQQKDkNvcHlyaWdodCBGbGFnAAAAAAEAADhCSU0nEBRKYXBhbmVzZSBQcmludCBGbGFncwAAAAAKAAEAAAAAAAAAAjhCSU0D9RdDb2xvciBIYWxmdG9uZSBTZXR0aW5ncwAAAEgAL2ZmAAEAbGZmAAYAAAAAAAEAL2ZmAAEAoZmaAAYAAAAAAAEAMgAAAAEAWgAAAAYAAAAAAAEANQAAAAEALQAAAAYAAAAAAAE4QklNA/gXQ29sb3IgVHJhbnNmZXIgU2V0dGluZ3MAAABwAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAADhCSU0ECAZHdWlkZXMAAAAAEAAAAAEAAAJAAAACQAAAAAA4QklNBB4NVVJMIG92ZXJyaWRlcwAAAAQAAAAAOEJJTQQaBlNsaWNlcwAAAAB3AAAABgAAAAAAAAAAAAAAWgAAAHgAAAALAG8AcABlAHIAYQBfAGgAbwB1AHMAZQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAeAAAAFoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEJJTQQREUlDQyBVbnRhZ2dlZCBGbGFnAAAAAQEAOEJJTQQUF0xheWVyIElEIEdlbmVyYXRvciBCYXNlAAAABAAAAAE4QklNBAwVTmV3IFdpbmRvd3MgVGh1bWJuYWlsAAALLwAAAAEAAABwAAAAVAAAAVAAAG5AAAALEwAYAAH/2P/gABBKRklGAAECAQBIAEgAAP/uAA5BZG9iZQBkgAAAAAH/2wCEAAwICAgJCAwJCQwRCwoLERUPDAwPFRgTExUTExgRDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBDQsLDQ4NEA4OEBQODg4UFA4ODg4UEQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAFQAcAMBIgACEQEDEQH/3QAEAAf/xAE/AAABBQEBAQEBAQAAAAAAAAADAAECBAUGBwgJCgsBAAEFAQEBAQEBAAAAAAAAAAEAAgMEBQYHCAkKCxAAAQQBAwIEAgUHBggFAwwzAQACEQMEIRIxBUFRYRMicYEyBhSRobFCIyQVUsFiMzRygtFDByWSU/Dh8WNzNRaisoMmRJNUZEXCo3Q2F9JV4mXys4TD03Xj80YnlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3EQACAgECBAQDBAUGBwcGBTUBAAIRAyExEgRBUWFxIhMFMoGRFKGxQiPBUtHwMyRi4XKCkkNTFWNzNPElBhaisoMHJjXC0kSTVKMXZEVVNnRl4vKzhMPTdePzRpSkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2JzdHV2d3h5ent8f/2gAMAwEAAhEDEQA/AOmAU2VPf9ETHJSDURr7GCG6BaJJ6NMeKJzdpjwTahTMkkn5pnTGunx0SvZHdKKcoMDw0lrhIjXRRrrdZu1gjkHlX2yK2DwaPyKFjQ/nQ/vDlVvvGpsDzDY9nQUT5FoPBa6FGXcSiW1uY4h2s8HxTPrex217S13gdFZiYkDY2wGwTvo2an1VMiQh5F1VjdsSeyBCUICAu7KuM1VMITQiEJoT7WMNqbapwlCNqf/Q6sBTZW95hgkjUplZxPovI5JA+5Xck+GJLVhHikAiZjWCxu8DbMnVWbX+09/xTmAQO5/ImcNCfAKpPIZ1daNiEBG66snlrWD4D8iC53igesXAGeQITbydBr5JlrkhcJDiNxaQ4D4K48V31gO9zHag9xPcKqzGfAdZpOjWjklHawVAMBkfx5KVnTwVQa4wXk+54A7dynODW1pc63Y1oLnvdDWtaBue97ifaxjfpK0NeFx3+Md/1it6fX03peHc/DySPtmXUN8ydteJtq3W11bvfc9zP0v80pffyfvfgFnsw7Ovi5VGZQ3KxiXY9hd6NhEb2NcWNu2/mtt272fyEWFHpODlMxG0WUfZKMcMpxhbHqvrYxodfbSwv9H1Ld/p1P8A0npfzv6RR631PovQMcX9UyjW5wmrHrAddZH+ipn/AMEs2VKwM8BEXLWtdGA4ZWaGl90kJQhYN1uVh05N1P2Z17fUFBO5zGO91LbXQz9N6Wx9vt9n82jwpQbAPdjqi//R6D6xX9Qx+n+r0p9NWRuaHeqN52nT9CyW/pGrnas3602YRZZ1U15DrRcLamBpDWtLPs3t/wAE/wDnLPalaMzJqpdl0323k7R7txpDnH9J6jj9Gpuz6KakZlVjab632jtkMaQyR+bcyT6Lv/An/mJZcspHSxFOPGIjWiXSwfrP1LG6Re/qbftnWatwoZWw7Lmy30f5oNbvb+k9T+bVKr/GcW2DG6p0v7K+0hm8Pe0N3e3dtsbY13P76Fdhm1hrc/IIJgu0a4T+axzam+z+V/4IqdJtN/2V1GW2qpz2G+xrHVbBP2d9T3U/ztm36DvoKMSOui4xHd62/qfT8PGORk5NdWPUIdYXAzHthjQdz3Pj2LGx/wDGNg02usdjUuxjGwjIAytv77qXsON7/wDRttVduIwmRZYSNTLav/edK2Mf03tNl++xtRa4MhrX+19x9Khrv0aQl4JMPF23/wCMD6tsZRkOdkuGRW6yqsVDeGjcLH2TZsb/ADbm/SVGz/Gf0h5Apw7yyRNll1LC3X6fpNdY93/FrPx8pt2TkUV1PaKNgNjmQHEn3Gt30XMYpWFja33C+6QC8MYWdtdjG+kXJcfgrg8Xrx1/o7Ayx+ZWGW1WXNM/mVAWW7o+g/Z9Gt300uk/WPpfV7jX019zrGNFjvUpfUdjtGmsv/nNzv3Fy13S3041Vue4ltx/mTZVc2RB22Nqr/N3LPtycnp4fZgMymWAij7NW4hz2Wbv0mM/32VtRBO1I4eoL0H1w+u/7GZZhdJr+09Qb7brtu6nGJ/NsI9luV/wX0Kv8KvOul+n1P6wY9vX8hxquuD8vJvl5cGje2l+jttdzw2r9yqtWG4+O3Edl2dKvrq3loabbd5MbnvcyPo7js9T8+z/AItV2ZWDW8XO6blBp9zQXOLYafcR6gc16cCQQaWEWKt9iIL/AHghweNwc2C0g67mub7dqbYV5vifWTIxqaqsa+7Bqa8PrxcmpzWF8+r7bqf55tn+Eru9Nlq1/wDnn1+gzk4+PY0Qfax7ZB/PZayx9StDmY9QQ1zy8uhBf//SC/pmKPoOIJ7Ne7+9MOnVNH85aB4ixytuDWHbMHwCeAAZPPYj+KsSxw/dH2NeM5dz9rXHTTGl14/tlI4FjSP1rI8hvn8FbDARMRroSf4Byk3cCQWkjsY5CiOOPYMgnLuWj9iuDdMy6PEkEf8AUqIx8sGRnWgdx7T/AN9WkS3ktOneFGAHTB1TeCPZdxy7uZkYXUrdhZ1S1gHGgjy42qpZ0jrTy6OruI7BzT+UPW+Tydp2nvIj/NTzXzHuHiATHyThCKDOXd5odE6wyNnUmeGrXAfDlMenfWMBv+UK3bRDY3cf5hXSOuLCP0YduMl2g/6MlRc2ppLgzbPYnUf2DuR9sI9yXd5l+P8AWVp3DMa4wRydPL3M9qGLPrQ1wAu3RwHODm/9L81dHZsk7WwSBogOaOQ2T3AMpwwx8VpzScYH63kfzm4Dgeo0/wDVOSFv1tYI2jaeRLII/lAXLcZu2CA4eIOhUgNCdr4/OiSnfd4eP4f96t+8T8H/05PZkB28OboPzi7/AL6p1OJEz8Cf9qC4vBEPY13jAJj71HZlfmPZH9Uz+VWZFrC240tPmR2EGfkkHMaQBo4nUOEx+IVXfcwAWmrXncQD9wCT8qn6IDNwGo2kg/cmEWuum4TYCI2OB8AZ/wCqc1DLniR7te7SQNP5YZ7VUD73O2iljmd/T0Mf5zXf9FGDrhpWAHRoHgx83Nc5NqlwNsn5uMyA+yHd9Hc+b9j0qs3FfoHbxoZa0kT/AJqGLuonSxrXDuGz/wBUXKX2jJA1phvYOg/IJwH8rWkn+QZzjuJdIYXcgkA6eA9Ru1QYamglu93jALv836X/AFSTb8h3/aQx5QP+qTWixxaHVWb+4B0H4ohHl+SxdU7hrpI5cDz/AGoQvdOjRH3H7lElu73bh81DfXJDXAHydJ/KpAsJbQIiCSPiRP4p2BocXAy7xMfwQCS0Ai+Y/N3NEfDcFJj73HVu8eHt/wC+7U+1r//UGz0tvs/nO+6N0/20x+3bvZt2x7d0fjuXl6SsS2aw3/g+qV/bJHq7Nvlz89ym7kT6cfPn+wvKElGN/wDvWQ7f98+rH7Rr6e3nSP8AzJB/WIHr7v8Arcx5bvzti8vSTvs/7pb9v7H039N6b9vq+XO3+xv/ADkAftLc3Z9q/wCjt+Xqe1ecpIlH2vp1vrbB9t3bZ1mN3/R/QquPT3D0/V2eUxH/AFK85STo/RbL6/V9Lb638rbP50Qmd6kGfS51mV5qknrX0+v0tv5v9nd/FPp+bO3vG3/0Z7l5ekih/9kAOEJJTQQhGlZlcnNpb24gY29tcGF0aWJpbGl0eSBpbmZvAAAAAFUAAAABAQAAAA8AQQBkAG8AYgBlACAAUABoAG8AdABvAHMAaABvAHAAAAATAEEAZABvAGIAZQAgAFAAaABvAHQAbwBzAGgAbwBwACAANgAuADAAAAABADhCSU0EBgxKUEVHIFF1YWxpdHkAAAAAB//+AAAAAQEA/+4ADkFkb2JlAGSAAAAAAf/bAIQAFBERGhIaKRgYKTMnICczJxwcHBwnIhcXFxcXIhEMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAEVGhohHSEiGBgiFA4ODhQUDg4ODhQRDAwMDAwREQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM/8AAEQgAWgB4AwEiAAIRAQMRAf/dAAQACP/EARsAAAMBAQEBAQEBAQEAAAAAAAEAAgMEBQYHCAkKCwEBAQEBAQEBAQEBAQEAAAAAAAECAwQFBgcICQoLEAACAgEDAgMEBwYDAwYCATUBAAIRAyESMQRBUSITYXEygZGxQqEF0cEU8FIjcjNi4YLxQzSSorIV0lMkc8JjBoOT4vKjRFRkJTVFFiZ0NlVls4TD03Xj80aUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm9hEAAgIABQEGBgEDAQMFAwYvAAERAiEDMUESUWFxgZEiEzLwobEEwdHh8UJSI2JyFJIzgkMkorI0U0Rjc8LSg5OjVOLyBRUlBhYmNWRFVTZ0ZbOEw9N14/NGlKSFtJXE1OT0pbXF1eX1VmZ2hv/aAAwDAQACEQMRAD8A9dIiTwoDYJi+o85BjSGibVoDGMjqFN3RdsOgbIvl5O8M6Kso5pRIYt2yRPycqd1cow8CoanV33gPMrWpCcFZCJcOdNIaiApFNUtNITSppUD/0PaSrpiGtvpbhScEpcGdF32gCmigvG1pOqrAwAAQSyZVoxucGiyWsVAEON3w6RgQbUgGTF3j9DIwyL0WrtXaRjijlnD04mUiAByw+L1f4kOt6uOCEqwYzuyS+zOWP/31ifcx/wA2InEGiLelbTqZtWNCVay7cETPJIRiO8izCQnESHBFu009DDTQqmlaQ//R9bqs8elh6kgSP8IeDH+MmUZShilYraJfbXP1ks8KiAI9/wDE82OYlpwfB1a+yJWu7PV6L8Tj1tjaYGHxCTcfxXpJGvUA/qEovjysWQDfjfxPBLpISu47SNfJJ5ybg+p9SMyZRIMTwQ55epxYK9WQjfD5GCWXBAQjZA/i2f8AbcJdNOUjMynuPe4KRB9Thy4px3YpCQ8Ync2ckMY88gP6jtfk49NGN5BM2QYk2xL8Ph2s1/T/AO/VIg+rx9RjykjHKMq/gludgbflMEJdLLdCwT7YNZo5swAlI7QTLbu/i/8AKmFSIPo/2TD6nqbI7/4trxfif4xi/Dxt+LKeIfw/+PfNH4hk6bp/Rx0JjieSXwx/9+PiT6eWQ7pGJJ5lu+JskOnpZZvxjqonMbjHzyj9iMY/7v8A8yP1z8b0ccvTz3wkIn2S+J9+P4vGNDLEg/bl/wDAnrSyRztVs9OleWH4n009BOv6gVevJdTnxZ//0sSCAImAoeBWRMzZhqOPM9BJPKu3RGFdmXqS7wP0uOSEMkt08crHte0Jq3PE1yOYZa+zJP7QB9mX0PTtZMXPEvI4JjHKO0idXu0QevgNdsg+iIrsbxHI82H4nCE9+3cRp5wjJ+JY8hJmDR+yH0vT91olgHgD8m8ScjxcuTps/wAYkaG0fv6TOWXTZQBUogaAD/40+ueniR8I+hyl00DzEfQ3iycjyPR6Q95gfv8A+UnaEsGPSE5iP8JO+P8A7rez9kx3rEN/smH+FvBk5o5Tl6aX2iFen9ixfw/eVXtsvNH/06J1oNA+DBjE9liaezOKNBomrLIl4JJLk0VXtQdGdDoQgHwclLGq34MkS7EfNz/mX2+9pDY252Y66tDepvwpoBd6uZtJmfH7mTO3SMsFW1TAIDYmPEOkZDEX3VIKtB//1J0lwLX04ns19l55c93qzkb+mBxakyHA+lxj/qeg/P5MGBnsMvY3t2jU2svm49+6YRcowJ5+9aA03UiXbhwyc9vmgdIG74ZLu2nWX3PHj57fJ6u3b/V8TQEzB+0iz73Lx4SP3pqMlX4hom+AQx37uw4/N0QmMfaqnnsqB//Z">
+ <animate attributeName="y" from="5" to="145" begin="100" dur="3" fill="freeze"/>
+ </image>
+ </defs>
+ <!-- END OF DEFS-->
+
+ <!-- ACTUAL TEST CONTENT-->
+ <use xlink:href="#lineID">
+ <animate attributeName="x" from="10" to="70" begin="100" dur="3" fill="freeze"/>
+ </use>
+
+ <use xlink:href="#rectID" transform="">
+ <animateTransform attributeName="transform" type="translate" from="0 0" to="140 0" begin="100" dur="3" fill="freeze"/>
+ <animateTransform attributeName="transform" type="scale" from="1 1" to="0.5 1" begin="100" dur="3" additive="sum" fill="freeze"/>
+ </use>
+
+ <use xlink:href="#circleID">
+ <animate attributeName="x" from="10" to="70" begin="100" dur="3" fill="freeze"/>
+ </use>
+
+ <use xlink:href="#polylineID" transform="">
+ <animateTransform attributeName="transform" type="rotate" from="0" to="15" additive="sum" begin="100" dur="3" fill="freeze"/>
+ </use>
+
+ <use x="60" y="-10" xlink:href="#polygonID" transform="">
+ <animateMotion path="M 0 0 l 0 150" begin="100" dur="3" fill="freeze"/>
+ <animateTransform attributeName="transform" type="scale" from="1 1" to="1 2" begin="100" dur="3" additive="sum" fill="freeze"/>
+ </use>
+
+ <use xlink:href="#imageID" transform="">
+ <animateTransform attributeName="transform" type="scale" from="1 .25" to="1 1" begin="100" dur="3" additive="sum" fill="freeze"/>
+ </use>
+ <!-- END OF ACTUAL TEST CONTENT-->
+ </g>
+
+ <text id="revision" x="10" y="340" font-size="40" stroke="none" fill="black">$Revision: 1.6 $</text>
+ <rect id="test-frame" x="1" y="1" width="478" height="358" fill="none" stroke="#000000"/>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+ class="reftest-wait"
+ onload="setTimeout(removeNode, 0)">
+ <script>
+ function removeNode() {
+ var node = document.getElementById("myRect");
+ node.parentNode.removeChild(node);
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+ <rect id="myRect" x="20" y="20" height="50" width="50" stroke="blue">
+ <animate attributeName="stroke-width" from="1" to="9" begin="0s" dur="2s"/>
+ </rect>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg">
+ <rect x="20" y="20" height="50" width="50" fill="blue">
+ <animate attributeName="display" by="inline"
+ begin="0s" dur="1s"/>
+ </rect>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ class="reftest-wait"
+ onload="setTimeout('boom()', 0)">
+<script type="text/javascript">
+<![CDATA[
+function boom()
+{
+ document.getElementById("anim").setAttribute("fill", "freeze");
+ document.documentElement.removeAttribute("class");
+}
+]]>
+</script>
+ <g transform="translate(50 50)">
+ <circle r="40" style="fill: yellow; stroke: black; stroke-width: 1">
+ <animate id="anim" attributeName="cx" attributeType="XML"
+ values="0; 200" dur="2s" begin="-1s" repeatCount="0.5"/>
+ </circle>
+ </g>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg">
+ <animate attributeName="fill-opacity" by="-1"/>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg">
+ <animate attributeName="fill-opacity" by="1"/>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <text y="20pt">abc
+ <animate attributeName="opacity" from="1" to="0" begin="0s" dur="2s"/>
+ </text>
+</svg>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <script>
+ var p = new XMLHttpRequest();
+ p.open("GET", "529387-1-helper.svg", false);
+ p.send();
+ </script>
+</html>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <g><animateTransform attributeName="transform" by="1"/></g>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" style="-moz-binding:url(#xbl)">
+<bindings xmlns="http://www.mozilla.org/xbl">
+<binding id="xbl" inheritstyle="false">
+<content>
+<svg xmlns="http://www.w3.org/2000/svg">
+<animate attributeName="font-size"/>
+</svg>
+</content>
+</binding>
+</bindings>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg"><svg id="w"><animate/></svg><script type="text/javascript">
+<![CDATA[
+
+function boom()
+{
+ anim = document.createElementNS("http://www.w3.org/2000/svg", "animate");
+ document.documentElement.appendChild(anim);
+ document.documentElement.removeChild(anim);
+
+ setTimeout(t, 0);
+
+ function t()
+ {
+ document.getElementById("w").appendChild(anim);
+ }
+}
+
+window.addEventListener("load", boom, false);
+
+]]>
+</script></svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+ class="reftest-wait">
+<script type="text/javascript">
+<![CDATA[
+
+function boom()
+{
+ document.getElementsByTagName("animate")[0].setAttributeNS(null, "attributeName", "font-size");
+ document.getElementsByTagName("text")[0].setAttributeNS(null, "fill", "green");
+ document.documentElement.removeAttributeNS(null, "x");
+ document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("load", boom, false);
+
+]]>
+</script>
+
+<text>abc<animate/></text>
+
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <circle id="circleID" cx="20" cy="100" r="10" fill="orange" stroke="black">
+ <animateTransform attributeName="transform" type="scale"
+ from="1" to="2" begin="0" dur="3"/>
+ </circle>
+ <rect id="rectID" fill="green" stroke="black" height="100" width="100">
+ <animate attributeName="fill" from="white" to="blue" begin="0" dur="3"/>
+ </rect>
+ </defs>
+ <use xlink:href="#circleID"/>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+
+<animate id="x" begin="y.end"/>
+<animate id="y"/>
+
+<script>
+
+function boom()
+{
+ var x = document.getElementById("x");
+ var y = document.getElementById("y");
+ y.appendChild(x);
+ y.setAttributeNS(null, "dur", "0.5s");
+ y.removeAttributeNS(null, "dur");
+}
+
+window.addEventListener("load", boom, false);
+
+</script>
+
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ onload="
+ document.documentElement.pauseAnimations();
+ document.documentElement.setCurrentTime(0);
+ document.getElementById('b').removeAttribute('begin');
+ document.getElementById('a').setAttribute('dur', '1s')">
+ <rect>
+ <animate attributeName="y" attributeType="XML" id="a"/>
+ <animate attributeName="fill" attributeType="CSS" id="b"
+ begin="a.end" dur="2s"/>
+ </rect>
+</svg>
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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg xmlns="http://www.w3.org/2000/svg">
+
+<bindings xmlns="http://www.mozilla.org/xbl">
+ <binding id="foo">
+ <content>
+ <animate xmlns="http://www.w3.org/2000/svg" begin="a.begin">
+ <children xmlns="http://www.mozilla.org/xbl"/>
+ </animate>
+ </content>
+ </binding>
+</bindings>
+
+<animate id="a" begin="b.begin; 3s"/>
+<animate id="b" begin="a.begin"/>
+
+<script type="text/javascript">
+
+function boom()
+{
+ document.documentElement.pauseAnimations();
+ document.documentElement.setCurrentTime(0);
+ document.getElementById('a').beginElementAt(1);
+ document.documentElement.style.MozBinding = 'url(#foo)';
+}
+
+window.addEventListener("load", boom, false);
+
+</script>
+
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ onload="
+ document.documentElement.pauseAnimations();
+ document.documentElement.setCurrentTime(0);
+ document.getElementById('a').beginElementAt(1);
+ document.documentElement.setCurrentTime(2)">
+ <!--
+ This test case sets up a cycle between simultaneous instance times such that
+ when the instance times are sorted if this cycle is not detected we will
+ crash.
+ -->
+ <rect width="100" height="100" fill="red">
+ <set attributeName="fill" to="blue" begin="a.begin" dur="4s"/>
+ <set attributeName="fill" to="green" id="a"
+ begin="b.begin; 3s" dur="4s"/>
+ <set attributeName="fill" to="red" id="b"
+ begin="a.begin" dur="4s"/>
+ </rect>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+ class="reftest-wait"
+ onload="go()">
+ <script>
+ function go() {
+ // setCurrentTime to force a sample
+ document.documentElement.setCurrentTime(1);
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+ <rect id="myRect" fill="blue" height="40" width="40">
+ <!-- The "keyTimes" values below are invalid, but they should be ignored
+ (and definitely shouldn't trigger any assertion failures) since we're
+ in paced calcMode. -->
+ <animate attributeName="x" by="50" calcMode="paced" dur="2s"
+ keyTimes="0; -1"/>
+ <animate attributeName="x" by="50" calcMode="paced" dur="2s"
+ keyTimes=""/>
+ <animate attributeName="x" by="50" calcMode="paced" dur="2s"
+ keyTimes="abc"/>
+ <animate attributeName="x" by="50" calcMode="paced" dur="2s"
+ keyTimes="5"/>
+ </rect>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+ class="reftest-wait"
+ onload="go()">
+ <script>
+ function go() {
+ // setCurrentTime to force a sample
+ document.documentElement.setCurrentTime(2);
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+ <rect fill="teal" x="50" y="50" width="20" height="20">
+ <animateTransform attributeName="transform" type="rotate" by="30"
+ calcMode="paced" dur="4s"/>
+ </rect>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <text id="myText">Used Text Element
+ <set attributeName="display" to="none"/>
+ </text>
+ </defs>
+ <use xlink:href="#myText" x="20" y="40"/>
+ <text x="20" y="60">Normal Text Element
+ <set attributeName="display" to="none"/>
+ </text>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ class="reftest-wait">
+
+ <script>
+ function boom()
+ {
+ document.getElementById("circleID").removeChild(
+ document.getElementById("at"));
+ document.documentElement.removeAttribute("class");
+ }
+ window.addEventListener("load", boom, false);
+ </script>
+
+ <circle id="circleID">
+ <animate/>
+ <animateTransform id="at" attributeName="transform"/>
+ </circle>
+ <animate attributeName="stroke-width"/>
+ <use xlink:href="#circleID"/>
+
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <text id="a">Text A</text>
+ <text id="b">Text B</text>
+ </defs>
+ <use xlink:href="#a" x="20" y="40">
+ <set attributeName="xlink:href" to="#b" dur="2s"/>
+ </use>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+<g id="a">
+ <path d=""><animate/></path>
+</g>
+<g display="none">
+<use xlink:href="#a" x="80"/>
+<set attributeName="display" to="inline"/>
+</g>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+<script>
+
+function boom()
+{
+ var animate = document.createElementNS("http://www.w3.org/2000/svg", "animate");
+ animate.setAttributeNS(null, "begin", "0.5s");
+ document.documentElement.appendChild(animate);
+
+ setTimeout(function() {
+ var g = document.createElement("g");
+ var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ g.appendChild(svg);
+ document.documentElement.appendChild(g);
+ svg.appendChild(animate);
+ document.documentElement.removeAttribute("class");
+ }, 400);
+}
+
+window.addEventListener("load", function() { setTimeout(boom, 200) }, false);
+
+</script>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+<script>
+
+function boom()
+{
+ var animate = document.createElementNS("http://www.w3.org/2000/svg", "animate");
+ animate.setAttributeNS(null, "begin", "0.5s");
+ document.documentElement.appendChild(animate);
+
+ setTimeout(function() {
+ var g = document.createElement("g");
+ var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ g.appendChild(svg);
+ document.documentElement.appendChild(g);
+ svg.setCurrentTime(0.2);
+ svg.appendChild(animate);
+ svg.setCurrentTime(0.0); // Trigger backwards seek
+ document.documentElement.removeAttribute("class");
+ }, 400);
+}
+
+window.addEventListener("load", function() { setTimeout(boom, 200) }, false);
+
+</script>
+</svg>
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 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+function boom()
+{
+ var frame = document.getElementById("frame")
+ var frameSVG = frame.contentDocument.getElementById('s');
+ var animate = frame.contentDocument.createElementNS("http://www.w3.org/2000/svg", "animate");
+ frame.parentNode.removeChild(frame);
+ frameSVG.appendChild(animate);
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+<body onload="boom()">
+
+<iframe id="frame" src="data:text/html,%3Cbody%3E%3Csvg id=s%3E"></iframe>
+
+</body>
+</html>
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 @@
+<xht:table xmlns:xht="http://www.w3.org/1999/xhtml"
+ xmlns:xbl="http://www.mozilla.org/xbl">
+ <xht:script type="text/javascript">// <![CDATA[
+ function boom() {
+ var topRight = document.getElementById('topRight');
+ topRight.appendChild(document.createElement("g"));
+ }
+//]]> </xht:script>
+
+<xbl:bindings>
+ <xbl:binding>
+ <svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ onload="setTimeout(boom, 0)">
+ <defs>
+ <g id="markerGroup">
+ <g id="topLeft"/>
+ <g id="topRight"/>
+ <set xlink:href="#topLeft" attributeName="fill" to="green" dur="1s"/>
+ </g>
+ </defs>
+ <use xlink:href="#markerGroup"/>
+ </svg>
+ </xbl:binding>
+</xbl:bindings>
+</xht:table>
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 @@
+<?xml version="1.0"?>
+
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+<script>
+<![CDATA[
+
+window.addEventListener("load", boom, false);
+
+function boom()
+{
+ setTimeout(bang, 0);
+}
+
+function bang()
+{
+ document.documentElement.setCurrentTime(0);
+ document.documentElement.removeAttribute("class");
+}
+
+]]>
+</script>
+
+<animate id="b"/>
+<animate end="b.end"/>
+
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+
+<script>
+function boom()
+{
+ document.documentElement.appendChild(document.getElementById("a"));
+ document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("load", boom, false);
+</script>
+
+<animate end="a.begin" id="a"/>
+
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+<script>
+<![CDATA[
+
+function boom()
+{
+ var anim = document.getElementById("a");
+ var newSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ var oldSvg = document.removeChild(document.documentElement);
+ document.appendChild(newSvg);
+ document.removeChild(document.documentElement);
+ newSvg.pauseAnimations();
+ document.appendChild(newSvg);
+ newSvg.appendChild(anim);
+
+ oldSvg.removeAttribute("class");
+}
+
+window.addEventListener("load", function() { setTimeout(boom, 200); }, false);
+
+]]>
+</script>
+<animate id="a"/>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg"
+ class="reftest-wait">
+<script>
+
+function boom()
+{
+ var origSVG = document.documentElement;
+
+ var a = document.createElementNS("http://www.w3.org/2000/svg", "animate");
+ var g = document.createElementNS("http://www.w3.org/2000/svg", "g");
+ var s = document.createElementNS("http://www.w3.org/2000/svg", "svg");
+ document.removeChild(document.documentElement);
+ document.appendChild(g);
+ s.appendChild(a);
+ g.appendChild(s);
+
+ origSVG.removeAttribute("class");
+}
+
+window.addEventListener("load", boom, false);
+
+</script>
+</svg>
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 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+function boom()
+{
+ // NB: <script src> is needed to trigger the bug. I'm being clever by also using it to remove reftest-wait.
+ var s = "<script src='data:text/javascript,parent.document.documentElement.className=null;'><\/script><svg>";
+ document.getElementById("f").contentDocument.write(s);
+}
+
+</script>
+</head>
+<body onload="boom();">
+<iframe id="f"></iframe>
+</body>
+</html>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+<script>
+<![CDATA[
+
+function boom()
+{
+ try {
+ document.getElementById("set").beginElementAt(NaN);
+ return;
+ } catch (e) {}
+ try {
+ document.getElementById("set").endElementAt(NaN);
+ return;
+ } catch (e) {}
+
+ // If we got here we threw both exceptions and skipped both early-returns, as
+ // expected.
+ document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("load", boom, false);
+
+]]>
+</script>
+
+<set id="set" attributeName="fill" to="green" begin="indefinite"/>
+
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <animate attributeName="stroke-width"/>
+ <animate attributeName="stroke-width" by="10em"/>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+<script>
+function boom()
+{
+ var a = document.getElementById("a");
+ a.removeAttribute('dur');
+ document.documentElement.appendChild(a);
+ // Force a sample
+ document.documentElement.setCurrentTime(0);
+ document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("load", boom, false);
+</script>
+<animate begin="-2s" dur="2s" id="a"/>
+</svg>
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 @@
+<?xml version="1.0"?>
+
+<svg xmlns="http://www.w3.org/2000/svg"><script>
+<![CDATA[
+
+function boom()
+{
+ var r = document.documentElement;
+ var s = document.createElementNS("http://www.w3.org/2000/svg", "set");
+ s.setAttributeNS(null, "begin", "1s");
+ r.appendChild(s);
+ r.setCurrentTime(2);
+ document.removeChild(r);
+ r.setCurrentTime(0);
+ s.beginElementAt(0);
+}
+
+window.addEventListener("load", boom, false);
+
+]]>
+</script></svg>
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 @@
+<script>
+
+var ar = new Array(100000);
+
+function fill() {
+ var s = unescape("%ubeef%udead%udead%udead%u0000%u0000%u3030%u3030");
+ while(s.length < 0x40000) {
+ for(var x=0; x<100; x++) ar.push(s+s);
+ s+=s;
+ }
+}
+
+
+function gc() {
+ var evt = document.createEvent("Events");
+ evt.initEvent("please-gc", true, false);
+ document.dispatchEvent(evt);
+ fill();
+}
+
+
+gc();
+function start(){
+tmp = document.createElement('iframe');
+tmp.src="data:image/svg+xml,"+escape("<?xml version='1.0' standalone='no'?><!DOCTYPE svg><svg xmlns='http://www.w3.org/2000/svg'><defs id='element1'></defs><g id='element5'></g></svg>");
+tmp.id = 'ifr23282';
+try{document.getElementById('store_div').appendChild(tmp);}catch(e){}
+window.setTimeout('startrly()', 100);
+} function startrly() {
+try{o6=document.createComment(null);}catch(e){}
+try{o9=document.getElementById('ifr23282').contentDocument.documentElement;;}catch(e){}
+try{o13=document.getElementById('ifr23282').contentDocument.getElementById('element1');;}catch(e){}
+try{o15=document.getElementById('ifr23282').contentDocument.getElementById('element5');;}catch(e){}
+try{tmp = document.createElement('iframe');}catch(e){}
+try{tmp.id = 'ifr6690';}catch(e){}
+try{o6.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe0()',100);
+} function start_dataiframe0(){
+try{o19=o6.ownerDocument.getElementById('ifr6690').contentDocument.documentElement;;}catch(e){}
+try{o24=document.createElementNS('http://www.w3.org/1998/Math/MathML','annotation-xml');;}catch(e){}
+try{o35=document.createElementNS('http://www.w3.org/1998/Math/MathML','emptyset');;}catch(e){}
+try{o40=o19.cloneNode(false);;}catch(e){}
+try{o19.appendChild(o13);}catch(e){}
+try{o19.appendChild(o15);}catch(e){}
+try{o24.appendChild(o40);}catch(e){}
+try{tmp = document.createElement('iframe');}catch(e){}
+tmp.src="data:text/html,<article%20id='element1'></article><command%20id='element3'></command>";
+try{tmp.id = 'ifr17516';}catch(e){}
+try{o13.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe4()',100);
+} function start_dataiframe4(){
+try{o62=o13.ownerDocument.getElementById('ifr17516').contentDocument.getElementById('element1');;}catch(e){}
+try{tmp.id = 'ifr2522';}catch(e){}
+try{o101=o15.ownerDocument.getElementById('ifr2522').contentDocument.getElementById('element3');;}catch(e){}
+try{o101.appendChild(o24);}catch(e){}
+try{o109=o35.setUserData('key',null,function (o,k,d,s,ds) { gc(); });;}catch(e){}
+try{o112=document.createElementNS('http://www.w3.org/1999/xhtml', 'script');;}catch(e){}
+try{o124=document.createElementNS('http://www.w3.org/1998/Math/MathML','root');;}catch(e){}
+try{o125=document.createElementNS('http://www.w3.org/2000/svg','font-face');;}catch(e){}
+gc()
+try{o150=o40;}catch(e){}
+try{tmp.id = 'ifr44501';}catch(e){}
+try{o124.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe7()',100);
+} function start_dataiframe7(){
+try{o152=o124.ownerDocument.getElementById('ifr44501').contentDocument.documentElement;;}catch(e){}
+try{tmp = document.createElement('iframe');}catch(e){}
+try{tmp.src="data:text/html,<div%20id='element1'></div>";}catch(e){}
+try{tmp.id = 'ifr55543';}catch(e){}
+try{o125.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe10()',100);
+} function start_dataiframe10(){
+try{o198=o125.ownerDocument.getElementById('ifr55543').contentDocument.getElementById('element1');;}catch(e){}
+try{o152.appendChild(o101);}catch(e){}
+try{o152.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe17()',100);
+} function start_dataiframe17(){
+try{o286=o152.ownerDocument.getElementById('ifr55543').contentDocument.documentElement;;}catch(e){}
+try{o288=o152.ownerDocument.getElementById('ifr55543').contentDocument.getElementById('element1');;}catch(e){}
+try{o349=document.createElementNS('http://www.w3.org/2000/svg','animate');;}catch(e){}
+try{o150.appendChild(o349);}catch(e){}
+try{o288.appendChild(o150);}catch(e){}
+try{o198.appendChild(o349);}catch(e){}
+window.setTimeout('start_dataiframe24()',100);
+} function start_dataiframe24(){
+try{o286.appendChild(o9);}catch(e){}
+try{o62.appendChild(o152);}catch(e){}
+try{o112.appendChild(o286);}catch(e){}
+try{o534=o35.cloneNode(false);;}catch(e){}
+gc();
+o35 = null;
+gc();
+window.setTimeout("fill()",300);
+}
+</script>
+<body onload="start()">
+<div id="store_div"></div>
+</body>
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 @@
+<script>
+function gc() {
+ var evt = document.createEvent("Events");
+ evt.initEvent("please-gc", true, false);
+ document.dispatchEvent(evt);
+ }
+var ar =new Array(100000);
+function fill() {
+ var s = unescape("%u0000%u0000%u3030%u3030");
+ while(s.length < 0x40000) {
+ for(var x=0; x<100; x++) ar.push(s+s);
+ s+=s;
+ }
+}
+
+
+function start(){
+tmp = document.createElement('iframe'); 'ifr16727';
+document.documentElement.appendChild(tmp);
+window.setTimeout('start_dataiframe0()',100);
+} function start_dataiframe0(){
+o20=document.createElement('iframe');
+tmp.id = 'ifr4446';;
+o68=o20;
+o101=document.getElementById('ifr4446').contentDocument.createElement('thead');;
+tmp.src="data:text/html," + escape("<html id='element0'><noscript id='element1'></html>");
+tmp.id = 'ifr49879';
+window.setTimeout('start_dataiframe6()',100);
+} function start_dataiframe6(){
+o104=document.getElementById('ifr49879').contentDocument.getElementById('element0');;
+o105=document.getElementById('ifr49879').contentDocument.getElementById('element1');;
+o120=document.getElementById('ifr49879').contentDocument.createElement('figure');;
+o105.appendChild(o120);
+o122=o105.lastElementChild;
+o140=document.getElementById('ifr49879').contentDocument.createElement('style');;
+o141=document.getElementById('ifr49879').contentDocument.createElementNS('http://www.w3.org/2000/svg','animate');;
+o151=o141.cloneNode(true);;
+tmp = document.createElement('iframe');
+tmp.src='data:text/html,%3Cform%20style%3B%27%20id%3D%27element3%27%3E%20%3Caside%20style%20id%3D%27element4%27%%3E';
+tmp.id = 'ifr13645';
+document.documentElement.appendChild(tmp);
+window.setTimeout('start_dataiframe8()',100);
+} function start_dataiframe8(){
+o154=document.getElementById('ifr13645').contentDocument.documentElement;;
+o158=document.getElementById('ifr13645').contentDocument.getElementById('element3');;
+o159=document.getElementById('ifr13645').contentDocument.getElementById('element4');;
+tmp.id = 'ifr17164';
+o120.ownerDocument.documentElement.appendChild(tmp);
+o171=o120.ownerDocument.getElementById('ifr17164').contentDocument.documentElement;;
+tmp = o158.ownerDocument.createElement('iframe');
+o101.appendChild(o151);
+o122.appendChild(o154);
+o68.appendChild(o171);
+o179=document.createElement('tbody');;
+o154.addEventListener('DOMNodeRemoved',function (event) { gc(); },false);
+tmp.src='data:text/html,%3Cs%27%20id%3D%27element0%27element4%27%3E%3Cs%20id%3D%27element5%27%20style%3D%27text-indent%3A%20-1%25%3Bmin-w%2C%20rgba%28255%2C0%2C0%2C0%29%20strict%3Bcolumn-count7element9%27%3E%s%3E';
+tmp.id = 'ifr35960';
+o154.ownerDocument.documentElement.appendChild(tmp);
+window.setTimeout('start_dataiframe13()',100);
+} function start_dataiframe13(){
+o217=o154.ownerDocument.getElementById('ifr35960').contentDocument.documentElement;;
+o218=o154.ownerDocument.getElementById('ifr35960').contentDocument.getElementById('element0');;
+o223=o154.ownerDocument.getElementById('ifr35960').contentDocument.getElementById('element5');;
+o223.appendChild(o101);
+o218.appendChild(o140);
+o140.appendChild(o151);
+o104.appendChild(o179);
+o230=o120.ownerDocument.getElementById('ifr17164').contentDocument.createElementNS('http://www.w3.org/2000/svg','svg');;
+window.setTimeout('start_dataiframe14()',100);
+} function start_dataiframe14(){
+gc();fill();
+o140.appendChild(o230);
+o171.appendChild(o104);
+o159.appendChild(o217);
+o158.appendChild(o218);
+}
+window.setTimeout("start()",100);
+</script>
+
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+ <rect fill="green" width="100" height="100">
+ <set id="a" attributeName="fill" to="blue"
+ begin="6s" end="986s"/>
+ <set id="b" attributeName="fill" to="orange"
+ begin="a.begin+69.3s;b.begin+700s" dur="700s" end="a.end"/>
+ <set id="c" attributeName="fill" to="yellow"
+ begin="0s;b.begin+700s"/>
+ </rect>
+ <script type="text/javascript">
+<![CDATA[
+const max_attempts = 100;
+var attempts = 0;
+function attemptCrash()
+{
+ remove();
+ add();
+ if (++attempts >= max_attempts) {
+ document.documentElement.removeAttribute("class");
+ } else {
+ setTimeout(attemptCrash, 0);
+ }
+}
+function add()
+{
+ const svgns = "http://www.w3.org/2000/svg";
+ var elem = document.createElementNS(svgns, "set");
+ elem.setAttribute("id", "b");
+ elem.setAttribute("attributeName", "fill");
+ elem.setAttribute("to", "orange");
+ elem.setAttribute("begin", "a.begin+69.3s;b.begin+700s");
+ elem.setAttribute("dur", "700s");
+ elem.setAttribute("end", "a.end");
+ rect = document.getElementsByTagNameNS(svgns, "rect")[0];
+ rect.appendChild(elem);
+}
+function remove()
+{
+ var elem = document.getElementById('b');
+ elem.parentNode.removeChild(elem);
+ elem = null;
+}
+window.addEventListener("load", attemptCrash, false);
+]]>
+ </script>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+<script>
+function boom()
+{
+ // Remove the first 'a'.
+ document.documentElement.removeChild(document.getElementById("a"));
+ document.documentElement.removeAttribute("class");
+}
+window.addEventListener("load", boom, false);
+</script>
+<animate id="a"/>
+<animate id="a" end="a.begin" />
+</svg>
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 @@
+<?xml version="1.0"?>
+
+<svg xmlns="http://www.w3.org/2000/svg">
+
+<script type="text/javascript">
+<![CDATA[
+
+function boom()
+{
+ document.documentElement.appendChild(document.getElementById("a"));
+}
+
+window.addEventListener("load", boom, false);
+
+]]>
+</script>
+
+<animate end="a.begin" id="a"/>
+<animate end="a.begin" id="a"/>
+
+</svg>
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 @@
+<?xml version="1.0"?>
+
+<svg xmlns="http://www.w3.org/2000/svg">
+
+<script type="text/javascript">
+<![CDATA[
+
+function boom()
+{
+ var a = document.getElementById("a");
+ a.removeAttribute("end");
+ a.setAttribute("end", "a.begin");
+}
+
+window.addEventListener("load", boom, false);
+
+]]>
+</script>
+
+<animate end="0" id="a" onend="boom()"/>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+ class="reftest-wait">
+<script type="text/javascript">
+<![CDATA[
+
+function boom()
+{
+ try {
+ document.getElementById("x").beginElementAt(36028797018963970);
+ } catch (e) { }
+ document.documentElement.removeAttribute("class");
+}
+
+window.addEventListener("load", boom, false);
+
+]]>
+</script>
+<animate id="x" begin="a" />
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <animate repeatCount="2" dur="1s" accumulate="1" />
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+<animate id="a" end="a.end+6s" />
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait">
+<script>
+ window.addEventListener("load", function() {
+ setTimeout(function() {
+ document.documentElement.setCurrentTime(0);
+ document.documentElement.removeAttribute("class");
+ }, 0);
+ }, false);
+</script>
+<set id="c"/><set id="b" begin="c.begin; b.begin"/>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait" onload="go()">
+<script>
+<![CDATA[
+function go() {
+ document.documentElement.setCurrentTime(100);
+}
+function boom()
+{
+ document.documentElement.removeChild(document.getElementById("a"));
+ document.documentElement.removeAttribute("class");
+}
+]]>
+</script>
+<animate id="a" begin="a.end; 99.9s" end="a.begin+0.2s" onend="boom()"/>
+<animate id="a" begin="a.end; 99.9s" end="a.begin+0.2s"/>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <rect width="100" height="100" fill="blue">
+ <animate attributeName="fill"
+ begin="999999999999999999999999999999999999999999999999999999999999999999999999999999999"
+ dur="5s" from="blue" to="red" repeatCount="indefinite" additive="sum"/>
+ </rect>
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
+ <rect width="100" height="100" fill="blue">
+ <animate attributeName="fill" id="a"
+ begin="4999999999999999" dur="5s" from="blue" to="red"
+ repeatCount="indefinite" additive="sum"/>
+ <animate attributeName="fill"
+ begin="a.begin+4999999999999999"
+ dur="5s" from="blue" to="red" repeatCount="indefinite" additive="sum"/>
+ </rect>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <animate id="b" end="b.end" dur="3s" />
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <rect fill="blue" height="100" width="100">
+ <animate id="a" attributeName="x" calcMode="paced" values="50; 50; 50"/>
+ </rect>
+</svg>
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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <animate attributeName="stroke-dasharray" from="-3" />
+</svg>
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 @@
+<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg">
+<animate id="a" begin="-3.1s" end="a.begin+0.2s"/>
+</svg>
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 <animateTransform>) the operation differs. For
+ * <animateTransform> 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 <algorithm>
+#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<nsIDocument> 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<bool> 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<nsSMILCompositorTable>
+ currentCompositorTable(new nsSMILCompositorTable(0));
+ nsTArray<RefPtr<SVGAnimationElement>>
+ 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<RefPtr<mozilla::dom::SVGAnimationElement>> 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<nsSMILTime>(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<nsIAtom> 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-<svg> 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<nsSMILTimeContainer> TimeContainerPtrKey;
+ typedef nsTHashtable<TimeContainerPtrKey> TimeContainerHashtable;
+ typedef nsPtrHashKey<mozilla::dom::SVGAnimationElement> AnimationElementPtrKey;
+ typedef nsTHashtable<AnimationElementPtrKey> 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 <animate> element is
+ // removed or retargeted)
+ nsAutoPtr<nsSMILCompositorTable> 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 <math.h>
+#include <algorithm>
+
+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 <set> 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<nsSMILValue> 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 // <animateMotion> 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<double> mKeyTimes;
+ FallibleTArray<nsSMILKeySpline> 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<nsComputedDOMStyle> 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 <use> 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<ValueWrapper*>(aValue.mU.mPtr);
+}
+
+static const ValueWrapper*
+ExtractValueWrapper(const nsSMILValue& aValue)
+{
+ return static_cast<const ValueWrapper*>(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<ValueWrapper*>(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<nsStyleContext> 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<nsISMILAttr> 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<nsISMILAttr> 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<nsSMILAnimationFunction*> 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<nsSMILValue> 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<nsSMILCompositor> 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 <math.h>
+
+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<nsSMILInstanceTime> 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<nsSMILInstanceTime> deathGrip(this);
+ mozilla::AutoRestore<bool> 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<nsSMILInstanceTime> 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<bool> 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<nsSMILInstanceTime>* 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<RefPtr<nsSMILInstanceTime> > 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<nsSMILInstanceTime> mBegin;
+ RefPtr<nsSMILInstanceTime> 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 <stdint.h>
+#include <math.h>
+
+#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<nsStringBuffer*>(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<nsIAtom> 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<nsIAtom> attrName = GetAttrNameAtom();
+ nsStringBuffer* oldValStrBuf = static_cast<nsStringBuffer*>
+ (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<nsIAtom> 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<nsIAtom>
+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<nsIAtom> 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<const char16_t>& aIter,
+ const RangedPtr<const char16_t>& aEnd)
+{
+ while (aIter != aEnd) {
+ if (!IsSVGWhitespace(*aIter)) {
+ return true;
+ }
+ ++aIter;
+ }
+ return false;
+}
+
+inline bool
+ParseColon(RangedPtr<const char16_t>& aIter,
+ const RangedPtr<const char16_t>& aEnd)
+{
+ if (aIter == aEnd || *aIter != ':') {
+ return false;
+ }
+ ++aIter;
+ return true;
+}
+
+/*
+ * Exactly two digits in the range 00 - 59 are expected.
+ */
+bool
+ParseSecondsOrMinutes(RangedPtr<const char16_t>& aIter,
+ const RangedPtr<const char16_t>& aEnd,
+ uint32_t& aValue)
+{
+ if (aIter == aEnd || !SVGContentUtils::IsDigit(*aIter)) {
+ return false;
+ }
+
+ RangedPtr<const char16_t> 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<const char16_t>& aIter,
+ const RangedPtr<const char16_t>& 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<const char16_t>& aIter,
+ const RangedPtr<const char16_t>& 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<const char16_t> 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<const char16_t>& aIter,
+ const RangedPtr<const char16_t>& aEnd,
+ nsSMILTimeValue* aResult)
+{
+ RangedPtr<const char16_t> 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<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec));
+ const RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));
+
+ return ParseOffsetValue(iter, end, aResult) && iter == end;
+}
+
+bool
+ParseOptionalOffset(RangedPtr<const char16_t>& aIter,
+ const RangedPtr<const char16_t>& 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<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec));
+ RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));
+
+ iter += ACCESSKEY_PREFIX_LC.Length();
+
+ // Expecting at least <accesskey> + ')'
+ 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<const char16_t>& aIter,
+ const RangedPtr<const char16_t>& 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<nsIAtom>
+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<nsIAtom>
+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<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aSpec));
+ RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));
+
+ if (start == end) {
+ return false;
+ }
+
+ RangedPtr<const char16_t> tokenEnd(start);
+
+ bool requiresUnescaping;
+ MoveToNextToken(tokenEnd, end, true, requiresUnescaping);
+
+ RefPtr<nsIAtom> 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<nsSMILKeySpline>& aKeySplines)
+{
+ nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> controlPointTokenizer(aSpec, ';');
+ while (controlPointTokenizer.hasMoreTokens()) {
+
+ nsCharSeparatedTokenizerTemplate<IsSVGWhitespace>
+ 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<double>& aArray)
+{
+ nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> 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<nsSMILValue>* 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<nsSMILValue>* mValuesArray;
+ bool* mPreventCachingOfSandwich;
+};
+
+bool
+nsSMILParserUtils::ParseValues(const nsAString& aSpec,
+ const SVGAnimationElement* aSrcElement,
+ const nsISMILAttr& aAttribute,
+ FallibleTArray<nsSMILValue>& 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<IsSVGWhitespace> 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<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec));
+ RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec));
+
+ return ::ParseClockValue(iter, end, aResult) && iter == end;
+}
+
+int32_t
+nsSMILParserUtils::CheckForNegativeNumber(const nsAString& aStr)
+{
+ int32_t absValLocation = -1;
+
+ RangedPtr<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aStr));
+ RangedPtr<const char16_t> iter = start;
+ RangedPtr<const char16_t> 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<nsSMILKeySpline>& aKeySplines);
+
+ // Used for parsing the |keyTimes| and |keyPoints| attributes.
+ static bool ParseSemicolonDelimitedProgressList(const nsAString& aSpec,
+ bool aNonDecreasing,
+ FallibleTArray<double>& aArray);
+
+ static bool ParseValues(const nsAString& aSpec,
+ const mozilla::dom::SVGAnimationElement* aSrcElement,
+ const nsISMILAttr& aAttribute,
+ FallibleTArray<nsSMILValue>& 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 <math.h>
+
+//----------------------------------------------------------------------
+// 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 <set> element is similar to <animate> 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 <set> 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 <set> 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;
+ }
+
+ // <set> 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<mozilla::dom::Element> mElement;
+ RefPtr<nsIAtom> 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 <algorithm>
+
+#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<nsSMILTime>(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<bool> 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<nsIContent*>(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<RefPtr<mozilla::dom::SVGAnimationElement>> elems;
+
+ {
+ AutoRestore<bool> 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<RefPtr<mozilla::dom::SVGAnimationElement> > 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<mozilla::dom::SVGAnimationElement> 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<MilestoneEntry> 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 <limits>
+
+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<Element> 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<nsSMILInstanceTime> 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<SVGAnimationElement*>(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<EventTarget> 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<nsSMILInstanceTime> 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<nsIDOMTimeEvent> 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<nsIDOMKeyEvent> 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<nsSMILTime>::max() ||
+ resultAsDouble < std::numeric_limits<nsSMILTime>::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<Element> 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<EventListener> 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<nsIAtom> mDependentElemID;
+
+ // The event to respond to.
+ // Only used for EVENT types.
+ RefPtr<nsIAtom> 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 <algorithm>
+
+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<nsIContent> 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<nsIPresShell> 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<RefPtr<nsSMILInstanceTime> > with O(n) performance.
+template <class TestFunctor>
+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<nsSMILInstanceTime>* 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<bool> 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<int32_t>(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<nsSMILTimeValueSpec>
+ 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<nsSMILInstanceTime> 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<const nsSMILInstanceTime *>& aTimesToKeep)
+ : mThreshold(aThreshold),
+ mTimesToKeep(aTimesToKeep) { }
+ bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t aIndex)
+ {
+ return aIndex < mThreshold && !mTimesToKeep.Contains(aInstanceTime);
+ }
+
+ private:
+ uint32_t mThreshold;
+ nsTArray<const nsSMILInstanceTime *>& 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<const nsSMILInstanceTime *> 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<nsSMILInstanceTime> tempBegin;
+ RefPtr<nsSMILInstanceTime> tempEnd;
+
+ while (true) {
+ // Calculate begin time
+ if (aFixedBeginTime) {
+ if (aFixedBeginTime->Time() < beginAfter) {
+ return false;
+ }
+ // our ref-counting is not const-correct
+ tempBegin = const_cast<nsSMILInstanceTime*>(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.
+ // <animate id="a" begin="b.begin; a.begin+2s"...
+ // If b's interval disappears whilst 'a' is in the waiting state the begin
+ // time at "a.begin+2s" should be skipped since 'a' never begun.
+ } while (aReplacedInterval &&
+ tempBegin->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<uint8_t> 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<uint8_t> 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<nsSMILInstanceTime> 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<nsIRunnable> 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<nsAutoPtr<nsSMILTimeValueSpec> > TimeValueSpecList;
+ typedef nsTArray<RefPtr<nsSMILInstanceTime> > InstanceTimeList;
+ typedef nsTArray<nsAutoPtr<nsSMILInterval> > IntervalList;
+ typedef nsPtrHashKey<nsSMILTimeValueSpec> TimeValueSpecPtrKey;
+ typedef nsTHashtable<TimeValueSpecPtrKey> 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 <class TestFunctor>
+ 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<nsSMILInterval> 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<nsSMILInterval> 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 <stdint.h>
+
+// 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 <string.h>
+
+//----------------------------------------------------------------------
+// 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 <animateMotion> */
+
+// 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 <svg>)
+ 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 <animateMotion> 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 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <filter id="filter">
+ <feFlood flood-color="red">
+ <set attributeName="flood-color" to="lime" begin="0.001"/>
+ </feFlood>
+ </filter>
+</svg>
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 <svg> 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: <animate> 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 <animateMotion> 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 <path> 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 @@
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px">
+ <circle id="circ" cx="20" cy="20" r="15" fill="blue">
+ <animate id="animXML" attributeName="cx" attributeType="XML"
+ from="500" to="600" begin="0s" dur="4s"/>
+ <animate id="animCSS" attributeName="opacity" attributeType="CSS"
+ from="0.2" to="0.3" begin="0s" dur="4s"/>
+ </circle>
+</svg>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL accessKey support</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=587910">Mozilla Bug
+ 587910</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px">
+ <circle cx="20" cy="20" r="15" fill="blue" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL accessKey support **/
+
+const gSvgns = "http://www.w3.org/2000/svg";
+var gSvg = document.getElementById("svg");
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ gSvg.pauseAnimations();
+
+ // Basic syntax
+ testOk('accessKey(a)', 'a');
+ testOk(' accessKey(a) ', 'a');
+ testNotOk('accessKey (a)', 'a');
+ testNotOk('accessKey( a)', 'a');
+ testNotOk('accessKey(a )', 'a');
+ testNotOk('accessKey(a)', 'b');
+ testNotOk('accessKey()', ' ');
+
+ // Test the test framework itself
+ testOk('accessKey(a)', 97);
+
+ // Allow for either accessKey (SVG / SMIL Animation) or accesskey (SMIL2+)
+ testOk('accesskey(a)', 'a');
+
+ // Offset
+ testOk('accessKey(a)+0s', 'a');
+ testOk('accessKey(a) + 0min', 'a');
+ testOk('accessKey(a) -0h', 'a');
+ testOk('accessKey(a)+100ms', 'a', 0, 0.1);
+ testOk('accessKey(a)-0.1s', 'a', 0, -0.1);
+
+ // Id references are not allowed
+ testNotOk('svg.accessKey(a)', 'a');
+ testNotOk('window.accessKey(a)', 'a');
+
+ // Case sensitivity
+ testOk('accessKey(A)', 'A');
+ testNotOk('accessKey(a)', 'A');
+ testNotOk('accessKey(A)', 'a');
+
+ // Test unusual characters
+ testOk('accessKey(-)', '-');
+ testOk('accessKey(\\)', '\\');
+ testOk('accessKey( )', ' ');
+ testOk('accessKey(\x0D)', 0, KeyboardEvent.DOM_VK_RETURN);
+ testOk('accessKey(\n)', 0, KeyboardEvent.DOM_VK_RETURN); // New line
+ testOk('accessKey(\r)', 0, KeyboardEvent.DOM_VK_RETURN); // Carriage return
+ testOk('accessKey(\x08)', 0, KeyboardEvent.DOM_VK_BACK_SPACE);
+ testOk('accessKey(\x1B)', 0, KeyboardEvent.DOM_VK_ESCAPE);
+ testOk('accessKey(\x7F)', 0, KeyboardEvent.DOM_VK_DELETE);
+
+ // Check some disallowed keys
+ // -- For now we don't allow tab since the interaction with focus causes
+ // confusing results
+ testNotOk('accessKey(\x09)', 0, 9); // Tab
+
+ // Test setting the keyCode field
+ testNotOk('accessKey(a)', 0, 97);
+ testOk('accessKey(a)', 97, 66); // Give priority to charCode field
+ testNotOk('accessKey(a)', 98, 97); // Give priority to charCode field
+
+ // Test unicode
+ testOk("accessKey(\u20AC)", 8364); // euro-symbol
+
+ // Test an astral character just to make sure we don't crash
+ testOk("accessKey(\uD835\uDC00)", 119808); // mathematical bold capital A
+ // 0x1D400
+ // Test bad surrogate pairs don't confuse us either
+ testNotOk("accessKey(\uD800\uD800)", 97);
+ testNotOk("accessKey(\uD80020)", 97);
+ testNotOk("accessKey(\uD800)", 97);
+
+ // Test modifiers
+ // -- When matching on charCode ignore shift and alt
+ testNotOk('accessKey(a)', 'a', 0, 0, { ctrl: true });
+ testNotOk('accessKey(a)', 'a', 0, 0, { meta: true });
+ testOk('accessKey(a)', 'a', 0, 0, { alt: true });
+ testOk('accessKey(a)', 'a', 0, 0, { shift: true });
+ testNotOk('accessKey(a)', 'a', 0, 0, { shift: true, ctrl: true });
+ testNotOk('accessKey(a)', 'a', 0, 0, { alt: true, meta: true });
+ // -- When matching on keyCode ignore all
+ testNotOk('accessKey(\x0D)', 0, 13, 0, { ctrl: true });
+ testNotOk('accessKey(\x0D)', 0, 13, 0, { meta: true });
+ testNotOk('accessKey(\x0D)', 0, 13, 0, { alt: true });
+ testNotOk('accessKey(\x0D)', 0, 13, 0, { shift: true });
+ testNotOk('accessKey(\x0D)', 0, 13, 0, { shift: true, ctrl: true });
+
+ testOpenEnd();
+ testPreventDefault();
+ testDispatchToWindow();
+ testAdoptNode();
+ testFauxEvent();
+
+ SimpleTest.finish();
+}
+
+function testOk(spec, charCode, keyCode, offset, modifiers)
+{
+ if (typeof offset == 'undefined') offset = 0;
+ var msg = "No interval created for '" + spec +
+ "' with input [charCode: " + charCode + "; keyCode: " + keyCode + "]" +
+ getModifiersDescr(modifiers);
+ ok(test(spec, charCode, keyCode, offset, modifiers), msg);
+}
+
+function testNotOk(spec, charCode, keyCode, offset, modifiers)
+{
+ if (typeof offset == 'undefined') offset = 0;
+ var msg = "Interval unexpectedly created for '" + spec +
+ "' with input [charCode: " + charCode + "; keyCode: " + keyCode + "]" +
+ getModifiersDescr(modifiers);
+ ok(!test(spec, charCode, keyCode, offset, modifiers), msg);
+}
+
+function getModifiersDescr(modifiers)
+{
+ if (typeof modifiers != 'object')
+ return '';
+ var str = ' modifiers set:';
+ for (var key in modifiers) {
+ if (modifiers[key]) str += ' ' + key;
+ }
+ return str;
+}
+
+function test(spec, charCode, keyCode, offset, modifiers)
+{
+ gSvg.setCurrentTime(1);
+ ok(gSvg.animationsPaused(), "Expected animations to be paused");
+
+ var anim = createAnim(spec);
+ var evt = createEvent(charCode, keyCode, modifiers);
+
+ document.getElementById('circle').dispatchEvent(evt);
+
+ var gotStartTimeOk = true;
+ try {
+ var start = anim.getStartTime();
+ if (offset) {
+ var expected = gSvg.getCurrentTime() + offset;
+ ok(Math.abs(expected - start) <= 0.00001,
+ "Unexpected start time for animation with begin: " + spec +
+ " got " + start + ", expected " + expected);
+ } else {
+ is(start, gSvg.getCurrentTime() + offset,
+ "Unexpected start time for animation with begin: " + spec);
+ }
+ } catch(e) {
+ is(e.name, "InvalidStateError",
+ "Unexpected exception: " + e.name);
+ is(e.code, DOMException.INVALID_STATE_ERR,
+ "Unexpected exception code: " + e.code);
+ gotStartTimeOk = false;
+ }
+
+ anim.parentNode.removeChild(anim);
+
+ return gotStartTimeOk;
+}
+
+function createAnim(beginSpec)
+{
+ var anim = document.createElementNS(gSvgns, 'animate');
+ anim.setAttribute('attributeName', 'cx');
+ anim.setAttribute('values', '0; 100');
+ anim.setAttribute('dur', '10s');
+ anim.setAttribute('begin', beginSpec);
+ return document.getElementById('circle').appendChild(anim);
+}
+
+function createEvent(charCode, keyCode, modifiers)
+{
+ if (typeof charCode == 'string') {
+ is(charCode.length, 1,
+ "If charCode is a string it should be 1 character long");
+ charCode = charCode.charCodeAt(0);
+ } else if (typeof charCode == 'undefined') {
+ charCode = 0;
+ }
+ args = { ctrl: false, alt: false, shift: false, meta: false };
+ if (typeof modifiers == 'object') {
+ for (var key in modifiers)
+ args[key] = modifiers[key];
+ }
+ if (typeof keyCode == 'undefined') keyCode = 0;
+ var evt = document.createEvent("KeyboardEvent");
+ evt.initKeyEvent("keypress", true, true, window,
+ args['ctrl'],
+ args['alt'],
+ args['shift'],
+ args['meta'],
+ keyCode,
+ charCode);
+ return evt;
+}
+
+function testOpenEnd()
+{
+ // Test that an end specification with an accesskey value is treated as open
+ // ended
+ gSvg.setCurrentTime(0);
+ ok(gSvg.animationsPaused(), "Expected animations to be paused");
+
+ var anim = createAnim('0s; 2s');
+ anim.setAttribute('end', '1s; accessKey(a)');
+
+ gSvg.setCurrentTime(2);
+
+ try {
+ is(anim.getStartTime(), 2,
+ "Unexpected start time for second interval of open-ended animation");
+ } catch(e) {
+ is(e.name, "InvalidStateError",
+ "Unexpected exception:" + e.name);
+ is(e.code, DOMException.INVALID_STATE_ERR,
+ "Unexpected exception code:" + e.code);
+ ok(false, "Failed to recognise accessKey as qualifying for creating an " +
+ "open-ended interval");
+ }
+
+ anim.parentNode.removeChild(anim);
+}
+
+function testPreventDefault()
+{
+ // SVG/SMIL don't specify what should happen if preventDefault is called on
+ // the keypress event. For now, for consistency with event timing we ignore
+ // it.
+ gSvg.setCurrentTime(1);
+ ok(gSvg.animationsPaused(), "Expected animations to be paused");
+
+ var anim = createAnim('accessKey(a)');
+ var evt = createEvent('a');
+
+ var circle = document.getElementById('circle');
+ var func = function(evt) { evt.preventDefault(); }
+ circle.addEventListener('keypress', func, false);
+ circle.dispatchEvent(evt);
+
+ try {
+ var start = anim.getStartTime();
+ } catch(e) {
+ ok(false, "preventDefault() cancelled accessKey handling");
+ }
+
+ circle.removeEventListener('keypress', func, false);
+ anim.parentNode.removeChild(anim);
+}
+
+function testDispatchToWindow()
+{
+ gSvg.setCurrentTime(1);
+ ok(gSvg.animationsPaused(), "Expected animations to be paused");
+
+ var anim = createAnim('accessKey(a)');
+ var evt = createEvent('a');
+
+ window.dispatchEvent(evt);
+
+ try {
+ var start = anim.getStartTime();
+ } catch(e) {
+ ok(false, "Key event dispatched to the window failed to trigger " +
+ "accesskey handling");
+ }
+
+ anim.parentNode.removeChild(anim);
+}
+
+function testAdoptNode()
+{
+ gSvg.setCurrentTime(1);
+ ok(gSvg.animationsPaused(), "Expected animations to be paused");
+
+ // Create a new document with an animation element
+ var newdoc = document.implementation.createDocument(gSvgns, 'svg', null);
+ var anim = newdoc.createElementNS(gSvgns, 'animate');
+ anim.setAttribute('attributeName', 'cx');
+ anim.setAttribute('values', '0; 100');
+ anim.setAttribute('dur', '10s');
+ anim.setAttribute('begin', 'accesskey(a)');
+ newdoc.documentElement.appendChild(anim);
+
+ // Adopt
+ ok(anim.ownerDocument !== document,
+ "Expected newly created animation to belong to a different doc");
+ document.adoptNode(anim);
+ document.getElementById('circle').appendChild(anim);
+ ok(anim.ownerDocument === document,
+ "Expected newly created animation to belong to the same doc");
+
+ var evt = createEvent('a');
+
+ // Now fire an event at the original window and check nothing happens
+ newdoc.dispatchEvent(evt);
+ try {
+ var start = anim.getStartTime();
+ ok(false, "Adopted node still receiving accesskey events from old doc");
+ } catch(e) {
+ // Ok
+ }
+
+ // And then fire at our window
+ document.dispatchEvent(evt);
+ try {
+ var start = anim.getStartTime();
+ } catch(e) {
+ ok(false, "Adopted node failed to catch accesskey event");
+ }
+
+ anim.parentNode.removeChild(anim);
+}
+
+function testFauxEvent()
+{
+ // Test a non-KeyEvent labelled as a key event
+ gSvg.setCurrentTime(0);
+ ok(gSvg.animationsPaused(), "Expected animations to be paused");
+
+ var anim = createAnim('accessKey(a)');
+ var evt = document.createEvent("SVGEvents");
+ evt.initEvent("keypress", true, true);
+ document.getElementById('circle').dispatchEvent(evt);
+
+ // We're really just testing that the above didn't crash us, but while we're
+ // at it, just do a sanity check that we didn't also create an interval
+ try {
+ var start = anim.getStartTime();
+ ok(false, "Faux event generated interval");
+ } catch(e) {
+ // All is well
+ }
+
+ anim.parentNode.removeChild(anim);
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=436418
+-->
+<head>
+ <title>Test for animateMotion behavior</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <script type="text/javascript" src="db_smilAnimateMotion.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436418">Mozilla Bug 436418</a>
+<p id="display"></p>
+<div id="content" style="visibility: hidden">
+
+<!-- NOTE: Setting font-size so we can test 'em' units -->
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="200px" height="200px" style="font-size: 500px"
+ onload="this.pauseAnimations()">
+ <!-- XXXdholbert Right now, 'em' conversions are correct if we set font-size
+ on rect using the inline style attr. However, if we use 'font-size' attr,
+ then 'em' units end up using the inherited font-size instead. Bug? -->
+ <rect x="20" y="20" width="200" height="200" style="font-size: 10px"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var timingData = new SMILTimingData(1.0, 6.0);
+ testBundleList(gMotionBundles, timingData);
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=436418
+-->
+<head>
+ <title>Test for animateMotion acceptance of invalid values</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js" />
+ <script type="text/javascript" src="smilAnimateMotionValueLists.js" />
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436418">Mozilla Bug 436418</a>
+<p id="display"></p>
+<div id="content" style="visibility: hidden">
+<svg xmlns="http://www.w3.org/2000/svg" id="svg"
+ width="200px" height="200px"
+ onload="this.pauseAnimations()">
+ <rect id="rect" x="20" y="20" width="200" height="200"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+// Constant strings (& string-arrays)
+const SVGNS = "http://www.w3.org/2000/svg";
+const XLINKNS = "http://www.w3.org/1999/xlink";
+
+// Constant objects
+const gSvg = document.getElementById("svg");
+const gRect = document.getElementById("rect");
+const gUnAnimatedCTM = gRect.getCTM();
+
+SimpleTest.waitForExplicitFinish();
+
+function createAnim()
+{
+ var anim = document.createElementNS(SVGNS, "animateMotion");
+ anim.setAttribute("dur", "2s");
+ return gRect.appendChild(anim);
+}
+
+function removeElem(aElem)
+{
+ aElem.parentNode.removeChild(aElem);
+}
+
+function testAttr(aAttrName, aAttrValueArray, aIsValid)
+{
+ var componentsToCheck;
+
+ for (var i in aAttrValueArray) {
+ var curVal = aAttrValueArray[i];
+ var anim = createAnim();
+ anim.setAttribute(aAttrName, curVal);
+ if (aAttrName == "rotate") {
+ // Apply a diagonal translation (so rotate='auto' will have an effect)
+ // and just test the rotation matrix components
+ anim.setAttribute("values", "0 0; 50 50");
+ componentsToCheck = CTMUtil.CTM_COMPONENTS_ROTATE;
+ } else {
+ // Apply a supplementary rotation to make sure that we don't apply it if
+ // our value is rejected.
+ anim.setAttribute("rotate", Math.PI/4);
+ componentsToCheck = CTMUtil.CTM_COMPONENTS_ALL;
+ if (aAttrName == "keyPoints") {
+ // Add three times so we can test a greater range of values for
+ // keyPoints
+ anim.setAttribute("values", "0 0; 25 25; 50 50");
+ anim.setAttribute("keyTimes", "0; 0.5; 1");
+ anim.setAttribute("calcMode", "discrete");
+ }
+ }
+
+ var curCTM = gRect.getCTM();
+ if (aIsValid) {
+ var errMsg = "CTM should have changed when applying animateMotion " +
+ "with '" + aAttrName + "' set to valid value '" + curVal + "'";
+ CTMUtil.assertCTMNotEqual(curCTM, gUnAnimatedCTM, componentsToCheck,
+ errMsg, false);
+ } else {
+ var errMsg = "CTM should not have changed when applying animateMotion " +
+ "with '" + aAttrName + "' set to invalid value '" + curVal + "'";
+ CTMUtil.assertCTMEqual(curCTM, gUnAnimatedCTM, componentsToCheck,
+ errMsg, false);
+ }
+ removeElem(anim);
+ }
+}
+
+function createPath(aPathDescription)
+{
+ var path = document.createElementNS(SVGNS, "path");
+ path.setAttribute("d", aPathDescription);
+ path.setAttribute("id", "thePath");
+ return gSvg.appendChild(path);
+}
+
+function createMpath(aAnimElement)
+{
+ var mpath = document.createElementNS(SVGNS, "mpath");
+ mpath.setAttributeNS(XLINKNS, "href", "#thePath");
+ return aAnimElement.appendChild(mpath);
+}
+
+function testMpathElem(aPathValueArray, aIsValid)
+{
+ for (var i in aPathValueArray) {
+ var curVal = aPathValueArray[i];
+ var anim = createAnim();
+ var mpath = createMpath(anim);
+ var path = createPath(curVal);
+
+ // Apply a supplementary rotation to make sure that we don't apply it if
+ // our value is rejected.
+ anim.setAttribute("rotate", Math.PI/4);
+ componentsToCheck = CTMUtil.CTM_COMPONENTS_ALL;
+
+ if (aIsValid) {
+ var errMsg = "CTM should have changed when applying animateMotion " +
+ "with mpath linking to a path with valid value '" + curVal + "'";
+
+ CTMUtil.assertCTMNotEqual(gRect.getCTM(), gUnAnimatedCTM,
+ componentsToCheck, errMsg, false);
+ } else {
+ var errMsg = "CTM should not have changed when applying animateMotion " +
+ "with mpath linking to a path with invalid value '" + curVal + "'";
+ CTMUtil.assertCTMEqual(gRect.getCTM(), gUnAnimatedCTM,
+ componentsToCheck, errMsg, false);
+ }
+ removeElem(anim);
+ removeElem(path);
+ removeElem(mpath);
+ }
+}
+
+// Main Function
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ testAttr("values", gValidValues, true);
+ testAttr("values", gInvalidValues, false);
+
+ testAttr("rotate", gValidRotate, true);
+ testAttr("rotate", gInvalidRotate, false);
+
+ testAttr("to", gValidToBy, true);
+ testAttr("to", gInvalidToBy, false);
+
+ testAttr("by", gValidToBy, true);
+ testAttr("by", gInvalidToBy, false);
+
+ testAttr("path", gValidPath, true);
+ testAttr("path", gInvalidPath, false);
+ testAttr("path", gValidPathWithErrors, true);
+
+ testAttr("keyPoints", gValidKeyPoints, true);
+ testAttr("keyPoints", gInvalidKeyPoints, false);
+
+ testMpathElem(gValidPath, true);
+ testMpathElem(gInvalidPath, false);
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=436418
+-->
+<head>
+ <title>Test for overriding of path-defining attributes for animateMotion</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js" />
+ <script type="text/javascript" src="smilAnimateMotionValueLists.js" />
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436418">Mozilla Bug 436418</a>
+<p id="display"></p>
+<div id="content" style="visibility: hidden">
+<svg xmlns="http://www.w3.org/2000/svg" id="svg"
+ width="200px" height="200px"
+ onload="this.pauseAnimations()">
+ <!-- Paths for mpath to refer to -->
+ <path id="validPathElem" d="M10 10 h-10"/>
+ <path id="invalidPathElem" d="abc"/>
+
+ <!-- The rect whose motion is animated -->
+ <rect id="rect" x="20" y="20" width="200" height="200"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+// Constant strings (& string-arrays)
+const SVGNS = "http://www.w3.org/2000/svg";
+const XLINKNS = "http://www.w3.org/1999/xlink";
+
+// Constant objects
+const gSvg = document.getElementById("svg");
+const gRect = document.getElementById("rect");
+const gUnAnimatedCTM = gRect.getCTM();
+
+// Values for path-defining attributes, and their expected
+// CTMs halfway through the animation
+var gMpathValidTarget = "#validPathElem";
+var gMpathCTM = CTMUtil.generateCTM([ 5, 10, 0 ]);
+
+var gMpathInvalidTargetA = "#invalidPathElem";
+var gMpathInvalidTargetB = "#nonExistentElem";
+
+var gInvalidAttrValue = "i-am-invalid"; // Invalid for all tested attributes
+
+var gPathValidValue = "M20 20 h10";
+var gPathCTM = CTMUtil.generateCTM([ 25, 20, 0 ]);
+
+var gValuesValidValue = "30 30; 40 30"
+var gValuesCTM = CTMUtil.generateCTM([ 35, 30, 0 ]);
+
+var gFromValidValue = "50 50";
+
+var gByValidValue = "10 2";
+var gPureByCTM = CTMUtil.generateCTM([ 5, 1, 0 ]);
+var gFromByCTM = CTMUtil.generateCTM([ 55, 51, 0 ]);
+
+var gToValidValue = "80 60";
+var gPureToCTM = CTMUtil.generateCTM([ 40, 30, 0 ]);
+var gFromToCTM = CTMUtil.generateCTM([ 65, 55, 0 ]);
+
+
+SimpleTest.waitForExplicitFinish();
+
+function createAnim()
+{
+ var anim = document.createElementNS(SVGNS, "animateMotion");
+ return gRect.appendChild(anim);
+}
+
+function removeElem(aElem)
+{
+ aElem.parentNode.removeChild(aElem);
+}
+
+function createMpath(aAnimElement, aHrefVal)
+{
+ var mpath = document.createElementNS(SVGNS, "mpath");
+ mpath.setAttributeNS(XLINKNS, "href", aHrefVal);
+ return aAnimElement.appendChild(mpath);
+}
+
+function runTest() {
+ // Start out with valid values for all path-defining attributes
+ var attrSettings = {
+ "mpath" : gMpathValidTarget,
+ "path" : gPathValidValue,
+ "values" : gValuesValidValue,
+ "from" : gFromValidValue,
+ "to" : gToValidValue,
+ "by" : gByValidValue,
+ };
+
+ // Test that <mpath> overrides everything below it
+ testAttrSettings(attrSettings, gMpathCTM,
+ "<mpath> should win");
+ var mpathInvalidTargets = [gMpathInvalidTargetA, gMpathInvalidTargetB];
+ for (var i in mpathInvalidTargets) {
+ var curInvalidValue = mpathInvalidTargets[i];
+ attrSettings["mpath"] = curInvalidValue;
+ testAttrSettings(attrSettings, gUnAnimatedCTM,
+ "invalid <mpath> should block animation");
+ }
+ delete attrSettings["mpath"];
+
+ // Test that 'path' overrides everything below it
+ testAttrSettings(attrSettings, gPathCTM,
+ "'path' should win vs all but mpath");
+ attrSettings["path"] = gInvalidAttrValue;
+ testAttrSettings(attrSettings, gUnAnimatedCTM,
+ "invalid 'path' should block animation vs all but mpath");
+ delete attrSettings["path"];
+
+ // Test that 'values' overrides everything below it
+ testAttrSettings(attrSettings, gValuesCTM,
+ "'values' should win vs from/by/to");
+ attrSettings["values"] = gInvalidAttrValue;
+ testAttrSettings(attrSettings, gUnAnimatedCTM,
+ "invalid 'values' should block animation vs from/by/to");
+ delete attrSettings["values"];
+
+ // Test that 'from' & 'to' overrides 'by'
+ testAttrSettings(attrSettings, gFromToCTM,
+ "'from/to' should win vs 'by'");
+ attrSettings["to"] = gInvalidAttrValue;
+ testAttrSettings(attrSettings, gUnAnimatedCTM,
+ "invalid 'to' should block animation vs 'by'");
+ delete attrSettings["to"];
+
+ // Test that 'from' & 'by' are effective
+ testAttrSettings(attrSettings, gFromByCTM,
+ "'from/by' should be visible");
+ attrSettings["by"] = gInvalidAttrValue;
+ testAttrSettings(attrSettings, gUnAnimatedCTM,
+ "invalid 'by' should block animation");
+ delete attrSettings["from"];
+
+ // REINSERT "to" & fix up "by" so we can test pure-"to" vs pure-"by"
+ attrSettings["to"] = gToValidValue;
+ attrSettings["by"] = gByValidValue;
+ testAttrSettings(attrSettings, gPureToCTM,
+ "pure-'to' should be effective & beat pure-'by'");
+ attrSettings["to"] = gInvalidAttrValue;
+ testAttrSettings(attrSettings, gUnAnimatedCTM,
+ "invalid pure-'to' should block animation vs pure-'by'");
+ delete attrSettings["to"];
+
+ // Test that pure-"by" is effective
+ testAttrSettings(attrSettings, gPureByCTM,
+ "pure-by should be visible");
+ attrSettings["by"] = gInvalidAttrValue;
+ testAttrSettings(attrSettings, gUnAnimatedCTM,
+ "invalid 'by' should block animation");
+ delete attrSettings["by"];
+
+ // Make sure that our hash is empty now.
+ for (var unexpectedKey in attrSettings) {
+ ok(false, "Unexpected mapping remains in attrSettings: " +
+ unexpectedKey + "-->" + unexpectedValue);
+ }
+}
+
+function testAttrSettings(aAttrValueHash, aExpectedCTM, aErrMsg)
+{
+ var isDebug = false; // XXdholbert
+ !isDebug || todo(false, "ENTERING testAttrSettings");
+ // Set up animateMotion element
+ var animElement = document.createElementNS(SVGNS, "animateMotion");
+ animElement.setAttribute("dur", "2s");
+ for (var attrName in aAttrValueHash) {
+ !isDebug || todo(false, "setting '" + attrName +"' to '" +
+ aAttrValueHash[attrName] +"'");
+ if (attrName == "mpath") {
+ createMpath(animElement, aAttrValueHash[attrName]);
+ } else {
+ animElement.setAttribute(attrName, aAttrValueHash[attrName]);
+ }
+ }
+
+ gRect.appendChild(animElement);
+
+ // Seek to halfway through animation
+ SMILUtil.getSVGRoot().setCurrentTime(1); // Seek halfway through animation
+
+ // Check CTM against expected value
+ CTMUtil.assertCTMEqual(gRect.getCTM(), aExpectedCTM,
+ CTMUtil.CTM_COMPONENTS_ALL, aErrMsg, false);
+
+ // CLEAN UP
+ SMILUtil.getSVGRoot().setCurrentTime(0);
+ removeElem(animElement);
+}
+
+// Main Function
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ runTest();
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for backwards seeking behavior </title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" />
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for backwards seeking behavior **/
+
+var gSvg = document.getElementById("svg");
+
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ // Pause our document, so that the setCurrentTime calls are the only
+ // thing affecting document time
+ gSvg.pauseAnimations();
+
+ // We define a series of scenarios, sample times, and expected return values
+ // from getStartTime.
+ //
+ // Each scenario is basically a variation on the following arrangement:
+ //
+ // <svg>
+ // <set ... dur="1s" begin="<A-BEGIN>"/>
+ // <set ... dur="1s" begin="<B-BEGIN>"/>
+ // </svg>
+ //
+ // Each test then consists of the following:
+ // animA: attributes to be applied to a
+ // animB: attributes to be applied to b
+ // times: a series of triples which consist of:
+ // <sample time, a's expected start time, b's expected start time>
+ // * The sample time is the time passed to setCurrentTime and so is
+ // in seconds.
+ // * The expected start times are compared with the return value of
+ // getStartTime. To check for an unresolved start time where
+ // getStartTime would normally throw an exception use
+ // 'unresolved'.
+ // * We also allow the special notation to indicate a call to
+ // beginElement
+ // <'beginElementAt', id of animation element, offset>
+ //
+ // In the diagrams below '^' means the time before the seek and '*' is the
+ // seek time.
+ var testCases = Array();
+
+ // 0: Simple case
+ //
+ // A: +-------
+ // B: +------- begin: a.begin
+ // * ^
+ testCases[0] = {
+ 'animA': {'begin':'1s', 'id':'a'},
+ 'animB': {'begin':'a.begin'},
+ 'times': [ [0, 1, 1],
+ [1, 1, 1],
+ [2, 'unresolved', 'unresolved'],
+ [0, 1, 1],
+ [1.5, 1, 1],
+ [1, 1, 1],
+ [2, 'unresolved', 'unresolved'] ]
+ };
+
+ // 1: Restored times should be live
+ //
+ // When we restore times they should be live. So we have the following
+ // scenario.
+ //
+ // A: +-------
+ // B: +------- begin: a.begin
+ // * ^
+ //
+ // Then we call beginElement at an earlier time which should give us the
+ // following.
+ //
+ // A: +-------
+ // B: +-------
+ // * ^
+ //
+ // If the times are not live however we'll end up with this
+ //
+ // A: +-------
+ // B: +-+-------
+ // * ^
+ testCases[1] = {
+ 'animA': {'begin':'1s', 'id':'a', 'restart':'whenNotActive'},
+ 'animB': {'begin':'a.begin', 'restart':'always'},
+ 'times': [ [0, 1, 1],
+ [2, 'unresolved', 'unresolved'],
+ [0.25, 1, 1],
+ ['beginElementAt', 'a', 0.25], // = start time of 0.5
+ [0.25, 0.5, 0.5],
+ [1, 0.5, 0.5],
+ [1.5, 'unresolved', 'unresolved'] ]
+ };
+
+ // 2: Multiple intervals A
+ //
+ // A: +- +-
+ // B: +- +- begin: a.begin+4s
+ // * ^
+ testCases[2] = {
+ 'animA': {'begin':'1s; 3s', 'id':'a'},
+ 'animB': {'begin':'a.begin+4s'},
+ 'times': [ [0, 1, 5],
+ [3, 3, 5],
+ [6.5, 'unresolved', 7],
+ [4, 'unresolved', 5],
+ [6, 'unresolved', 7],
+ [2, 3, 5],
+ ['beginElementAt', 'a', 0],
+ [2, 2, 5],
+ [5, 'unresolved', 5],
+ [6, 'unresolved', 6],
+ [7, 'unresolved', 7],
+ [8, 'unresolved', 'unresolved'] ]
+ };
+
+ for (var i = 0; i < testCases.length; i++) {
+ gSvg.setCurrentTime(0);
+ var test = testCases[i];
+
+ // Create animation elements
+ var animA = createAnim(test.animA);
+ var animB = createAnim(test.animB);
+
+ // Run samples
+ for (var j = 0; j < test.times.length; j++) {
+ var times = test.times[j];
+ if (times[0] == 'beginElementAt') {
+ var anim = getElement(times[1]);
+ anim.beginElementAt(times[2]);
+ } else {
+ gSvg.setCurrentTime(times[0]);
+ checkStartTime(animA, times[1], times[0], i, 'a');
+ checkStartTime(animB, times[2], times[0], i, 'b');
+ }
+ }
+
+ // Tidy up
+ animA.parentNode.removeChild(animA);
+ animB.parentNode.removeChild(animB);
+ }
+
+ SimpleTest.finish();
+}
+
+function createAnim(attr)
+{
+ const svgns = "http://www.w3.org/2000/svg";
+ var anim = document.createElementNS(svgns, 'set');
+ anim.setAttribute('attributeName','x');
+ anim.setAttribute('to','10');
+ anim.setAttribute('dur','1s');
+ for (name in attr) {
+ anim.setAttribute(name, attr[name]);
+ }
+ return gSvg.appendChild(anim);
+}
+
+function checkStartTime(anim, expectedStartTime, sampleTime, caseNum, id)
+{
+ var startTime = 'unresolved';
+ try {
+ startTime = anim.getStartTime();
+ } catch (e) {
+ if (e.name != "InvalidStateError" ||
+ e.code != DOMException.INVALID_STATE_ERR)
+ throw e;
+ }
+
+ var msg = "Test case " + caseNum + ", t=" + sampleTime + " animation '" +
+ id + "': Unexpected getStartTime:";
+ is(startTime, expectedStartTime, msg);
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Animation Behavior on CSS Properties</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg">
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/* This testcase verifies that animated values of "wider" and "narrower" for
+ "font-stretch" have the expected effect, across all possible inherited
+ values of the property.
+ XXXdholbert Currently, we don't support animating relative values of
+ font-stretch, so most of the tests here use todo_is() rather than is().
+*/
+
+SimpleTest.waitForExplicitFinish();
+
+const gPropertyName="font-stretch";
+
+// List of non-relative font-stretch values, from smallest to largest
+const gFontStretchValues = [
+ "ultra-condensed",
+ "extra-condensed",
+ "condensed",
+ "semi-condensed",
+ "normal",
+ "semi-expanded",
+ "expanded",
+ "extra-expanded",
+ "ultra-expanded"
+];
+
+function testFontStretchValue(baseValue, narrowerStep, widerStep)
+{
+ var svg = SMILUtil.getSVGRoot();
+ var gElem = document.createElementNS(SVG_NS, "g");
+ gElem.setAttribute("style", "font-stretch: " + baseValue);
+ svg.appendChild(gElem);
+
+ var textElem = document.createElementNS(SVG_NS, "text");
+ gElem.appendChild(textElem);
+
+ var animElem = document.createElementNS(SVG_NS, "set");
+ animElem.setAttribute("attributeName", gPropertyName);
+ animElem.setAttribute("attributeType", "CSS");
+ animElem.setAttribute("begin", "0s");
+ animElem.setAttribute("dur", "indefinite");
+ textElem.appendChild(animElem);
+
+ // CHECK EFFECT OF 'narrower'
+ // NOTE: Using is() instead of todo_is() for ultra-condensed, since
+ // 'narrower' has no effect on that value.
+ var myIs = (baseValue == "ultra-condensed" ? is : todo_is);
+ animElem.setAttribute("to", "narrower");
+ SMILUtil.getSVGRoot().setCurrentTime(1.0); // Force a resample
+ myIs(SMILUtil.getComputedStyleSimple(textElem, gPropertyName), narrowerStep,
+ "checking effect of 'narrower' on inherited value '" + baseValue + "'");
+
+ // CHECK EFFECT OF 'wider'
+ // NOTE: using is() instead of todo_is() for ultra-expanded, since
+ // 'wider' has no effect on that value.
+ myIs = (baseValue == "ultra-expanded" ? is : todo_is);
+ animElem.setAttribute("to", "wider");
+ SMILUtil.getSVGRoot().setCurrentTime(1.0); // Force a resample
+ myIs(SMILUtil.getComputedStyleSimple(textElem, gPropertyName), widerStep,
+ "checking effect of 'wider' on inherited value '" + baseValue + "'");
+
+ // Removing animation should clear animated effects
+ textElem.removeChild(animElem);
+ svg.removeChild(gElem);
+}
+
+function main()
+{
+ var valuesList = gFontStretchValues;
+ for (var baseIdx in valuesList) {
+ // 'narrower' and 'wider' are expected to shift us by one slot, but not
+ // past the ends of the list of possible values.
+ var narrowerIdx = Math.max(baseIdx - 1, 0);
+ var widerIdx = Math.min(baseIdx + 1, valuesList.length - 1);
+
+ testFontStretchValue(valuesList[baseIdx],
+ valuesList[narrowerIdx], valuesList[widerIdx]);
+ }
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Animation Behavior on CSS Properties</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <script type="text/javascript" src="db_smilCSSPropertyList.js"></script>
+ <script type="text/javascript" src="db_smilCSSFromBy.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)"
+ onload="this.pauseAnimations()">
+ <rect x="20" y="20" width="200" height="200"/>
+ <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is
+ more predictable. (otherwise, line-height varies depending on platform)
+ -->
+ <text x="20" y="20" style="line-height: 10px !important">testing 123</text>
+ <line/>
+ <marker/>
+ <filter><feDiffuseLighting/></filter>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ testBundleList(gFromByBundles, new SMILTimingData(1.0, 1.0));
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Animation Behavior on CSS Properties</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <script type="text/javascript" src="db_smilCSSPropertyList.js"></script>
+ <script type="text/javascript" src="db_smilCSSFromTo.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)"
+ onload="this.pauseAnimations()">
+ <rect x="20" y="20" width="200" height="200"/>
+ <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is
+ more predictable. (otherwise, line-height varies depending on platform)
+ -->
+ <text x="20" y="20" style="line-height: 10px !important">testing 123</text>
+ <line/>
+ <image/>
+ <marker/>
+ <clipPath><circle/></clipPath>
+ <filter><feFlood/></filter>
+ <filter><feDiffuseLighting/></filter>
+ <linearGradient><stop/></linearGradient>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+function checkForUntestedProperties(bundleList)
+{
+ // Create the set of all the properties we know about
+ var propertySet = {};
+ for (propertyLabel in gPropList) {
+ // insert property
+ propertySet[gPropList[propertyLabel].attrName] = null;
+ }
+ // Remove tested properties from the set
+ for (var bundleIdx in bundleList) {
+ var bundle = bundleList[bundleIdx];
+ delete propertySet[bundle.animatedAttribute.attrName];
+ }
+ // Warn about remaining (untested) properties
+ for (var untestedProp in propertySet) {
+ ok(false, "No tests for property '" + untestedProp + "'");
+ }
+}
+
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ // FIRST: Warn about any properties that are missing tests
+ checkForUntestedProperties(gFromToBundles);
+
+ // Run the actual tests
+ testBundleList(gFromToBundles, new SMILTimingData(1.0, 1.0));
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Animation Behavior on CSS Properties</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="300px" height="200px"
+ onload="this.pauseAnimations()">
+ <!-- At 50% through the animation, the following should be true:
+ * First <g> has font-size = 5px (1/2 between 0px and 10px)
+ * Next <g> has font-size = 10px (1/2 between inherit=5px and 15px)
+ * Next <g> has font-size = 15px (1/2 between inherit=10px and 20px)
+ * Next <g> has font-size = 20px (1/2 between inherit=15px and 25px)
+ * Next <g> has font-size = 25px (1/2 between inherit=20px and 30px)
+ * Next <g> has font-size = 30px (1/2 between inherit=25px and 35px)
+ * Next <g> has font-size = 35px (1/2 between inherit=30px and 40px)
+ * Next <g> has font-size = 40px (1/2 between inherit=35px and 45px)
+ * Next <g> has font-size = 45px (1/2 between inherit=40px and 50px)
+ * Next <g> has font-size = 50px (1/2 between inherit=45px and 55px)
+ * <text> has font-size = 75px (1/2 between inherit=50px and 100px)
+ -->
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="0px" to="10px" begin="0s" dur="1s"/>
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="15px" begin="0s" dur="1s"/>
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="20px" begin="0s" dur="1s"/>
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="25px" begin="0s" dur="1s"/>
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="30px" begin="0s" dur="1s"/>
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="35px" begin="0s" dur="1s"/>
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="40px" begin="0s" dur="1s"/>
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="45px" begin="0s" dur="1s"/>
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="50px" begin="0s" dur="1s"/>
+ <g><animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="55px" begin="0s" dur="1s"/>
+ <text y="100px" x="0px">
+ abc
+ <animate attributeName="font-size" attributeType="CSS"
+ from="inherit" to="100px" begin="0s" dur="1s"/>
+ </text></g></g></g></g></g></g></g></g></g></g>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ // Pause & seek to halfway through animation
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+ svg.setCurrentTime(0.5);
+
+ var text = document.getElementsByTagName("text")[0];
+ var computedVal = SMILUtil.getComputedStyleSimple(text, "font-size");
+ var expectedVal = "75px";
+
+ // NOTE: There's a very small chance (1/11! = 1/39,916,800) that we'll happen
+ // to composite our 11 animations in the correct order, in which cast this
+ // "todo_is" test would sporadically pass. I think this is infrequent enough
+ // to accept as a sporadic pass rate until this bug is fixed (at which point
+ // this "todo_is" will become an "is")
+ todo_is(computedVal, expectedVal,
+ "deeply-inherited font-size halfway through animation");
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Animation Behavior on CSS Properties</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <script type="text/javascript" src="db_smilCSSPropertyList.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg xmlns="http://www.w3.org/2000/svg"
+ onload="this.pauseAnimations()">
+ <rect x="20" y="20" width="200" height="200"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+var invalidTestcaseBundles = [
+ new TestcaseBundle(gPropList.opacity, [
+ new AnimTestcaseFromTo("", "", { noEffect: true }),
+ new AnimTestcaseFromTo("", "0.5", { noEffect: true }),
+ new AnimTestcaseFromTo(".", "0.5", { noEffect: true }),
+ new AnimTestcaseFromTo("0.5", "-", { noEffect: true }),
+ new AnimTestcaseFromTo("0.5", "bogus", { noEffect: true }),
+ new AnimTestcaseFromTo("bogus", "bogus", { noEffect: true }),
+ ]),
+ new TestcaseBundle(gPropList.color, [
+ new AnimTestcaseFromTo("", "", { noEffect: true }),
+ new AnimTestcaseFromTo("", "red", { noEffect: true }),
+ new AnimTestcaseFromTo("greeeen", "red", { noEffect: true }),
+ new AnimTestcaseFromTo("rgb(red, 255, 255)", "red", { noEffect: true }),
+ new AnimTestcaseFromTo("#FFFFFFF", "red", { noEffect: true }),
+ new AnimTestcaseFromTo("bogus", "bogus", { noEffect: true }),
+ ]),
+];
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ // Run the tests
+ testBundleList(invalidTestcaseBundles, new SMILTimingData(1.0, 1.0));
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Animation Behavior on CSS Properties</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <script type="text/javascript" src="db_smilCSSPropertyList.js"></script>
+ <script type="text/javascript" src="db_smilCSSPaced.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)"
+ onload="this.pauseAnimations()">
+ <rect x="20" y="20" width="200" height="200"/>
+ <text x="20" y="20">testing 123</text>
+ <marker/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ testBundleList(gPacedBundles, new SMILTimingData(1.0, 6.0));
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL when things change after an animation is frozen</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=533291">Mozilla Bug 533291</a>
+<p id="display"></p>
+<!-- Bug 628848: The following should be display: none but we currently don't
+ handle percentage lengths properly when the whole fragment is display: none
+ -->
+<div id="content" style="visibility: hidden">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <g id="circleParent">
+ <circle cx="0" cy="20" r="15" fill="blue" id="circle"/>
+ </g>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL values that are context-sensitive **/
+
+/* See bugs 533291 and 562815.
+
+ The format of each test is basically:
+ 1) create some animated and frozen state
+ 2) test the animated values
+ 3) change the context
+ 4) test that context-sensitive animation values have changed
+
+ Ideally, after changing the context (3), the animated state would instantly
+ update. However, this is not currently the case for many situations.
+
+ For CSS properties we have bug 545282 - In animations involving 'inherit'
+ / 'currentColor', changes to inherited value / 'color' don't show up in
+ animated value immediately
+
+ For SVG lengths we have bug 508206 - Relative units used in
+ animation don't update immediately
+
+ (There are a few of todo_is's in the following tests so that if those bugs
+ are ever resolved we'll know to update this test case accordingly.)
+
+ So in between (3) and (4) we force a sample. This is currently done by
+ calling SVGSVGElement.setCurrentTime with the same current time which has the
+ side effect of forcing a sample.
+
+ What we *are* testing is that we're not too zealous with caching animation
+ values whilst in the frozen state. Normally we'd say, "Hey, we're frozen,
+ let's just use the same animation result as last time" but for some
+ context-sensitive animation values that doesn't work.
+*/
+
+/* Global Variables */
+const SVGNS = "http://www.w3.org/2000/svg";
+
+// Animation parameters -- not used for <set> animation
+const ANIM_DUR = "4s";
+const TIME_ANIM_END = "4";
+const TIME_AFTER_ANIM_END = "5";
+
+const gSvg = document.getElementById("svg");
+const gCircle = document.getElementById("circle");
+const gCircleParent = document.getElementById("circleParent");
+
+SimpleTest.waitForExplicitFinish();
+
+// MAIN FUNCTION
+// -------------
+
+function main()
+{
+ ok(gSvg.animationsPaused(), "should be paused by <svg> load handler");
+ is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ const tests =
+ [ testBaseValueChange,
+ testCurrentColorChange,
+ testCurrentColorChangeUsingStyle,
+ testCurrentColorChangeOnFallback,
+ testInheritChange,
+ testInheritChangeUsingStyle,
+ testEmUnitChangeOnProp,
+ testEmUnitChangeOnPropBase,
+ testEmUnitChangeOnLength,
+ testPercentUnitChangeOnProp,
+ testPercentUnitChangeOnLength,
+ testRelativeFontSize,
+ testRelativeFontWeight,
+ testRelativeFont,
+ testCalcFontSize,
+ testDashArray,
+ testClip
+ ];
+
+ while (tests.length) {
+ tests.shift()();
+ }
+ SimpleTest.finish();
+}
+
+// HELPER FUNCTIONS
+// ----------------
+function createAnimSetTo(attrName, toVal)
+{
+ var anim = document.createElementNS(SVGNS,"set");
+ anim.setAttribute("attributeName", attrName);
+ anim.setAttribute("to", toVal);
+ return gCircle.appendChild(anim);
+}
+
+function createAnimBy(attrName, byVal)
+{
+ var anim = document.createElementNS(SVGNS,"animate");
+ anim.setAttribute("attributeName", attrName);
+ anim.setAttribute("dur", ANIM_DUR);
+ anim.setAttribute("begin","0s");
+ anim.setAttribute("by", byVal);
+ anim.setAttribute("fill", "freeze");
+ return gCircle.appendChild(anim);
+}
+
+function createAnimFromTo(attrName, fromVal, toVal)
+{
+ var anim = document.createElementNS(SVGNS,"animate");
+ anim.setAttribute("attributeName", attrName);
+ anim.setAttribute("dur", ANIM_DUR);
+ anim.setAttribute("begin","0s");
+ anim.setAttribute("from", fromVal);
+ anim.setAttribute("to", toVal);
+ anim.setAttribute("fill", "freeze");
+ return gCircle.appendChild(anim);
+}
+
+// Common setup code for each test function: seek to 0, and make sure
+// the previous test cleaned up its animations.
+function setupTest() {
+ gSvg.setCurrentTime(0);
+ if (gCircle.firstChild) {
+ ok(false, "Previous test didn't clean up after itself.");
+ }
+}
+
+// THE TESTS
+// ---------
+
+function testBaseValueChange()
+{
+ setupTest();
+ var anim = createAnimBy("cx", "50");
+ gSvg.setCurrentTime(TIME_ANIM_END);
+ is(gCircle.cx.animVal.value, 50,
+ "Checking animated cx as anim ends");
+
+ gSvg.setCurrentTime(TIME_AFTER_ANIM_END);
+ is(gCircle.cx.animVal.value, 50,
+ "Checking animated cx after anim ends");
+
+ gCircle.setAttribute("cx", 20);
+ is(gCircle.cx.animVal.value, 70,
+ "Checking animated cx after anim ends & after changing base val");
+
+ anim.parentNode.removeChild(anim); // clean up
+}
+
+function testCurrentColorChange()
+{
+ gCircle.setAttribute("color", "red"); // At first: currentColor=red
+ var anim = createAnimSetTo("fill", "currentColor");
+
+ gSvg.setCurrentTime(0); // trigger synchronous sample
+ is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)",
+ "Checking animated fill=currentColor after animating");
+
+ gCircle.setAttribute("color", "lime"); // Change: currentColor=lime
+ // Bug 545282: We should really detect this change and update immediately but
+ // currently we don't until we get sampled again
+ todo_is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)",
+ "Checking animated fill=currentColor after updating context but before " +
+ "sampling");
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)",
+ "Checking animated fill=currentColor after updating context");
+
+ // Clean up
+ gCircle.removeAttribute("color");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testCurrentColorChangeUsingStyle()
+{
+ setupTest();
+ gCircle.setAttribute("style", "color: red"); // At first: currentColor=red
+ var anim = createAnimSetTo("fill", "currentColor");
+
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)",
+ "Checking animated fill=currentColor after animating (using style attr)");
+
+ gCircle.setAttribute("style", "color: lime"); // Change: currentColor=lime
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)",
+ "Checking animated fill=currentColor after updating context "
+ + "(using style attr)");
+
+ // Clean up
+ gCircle.removeAttribute("style");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function getFallbackColor(pServerStr)
+{
+ return pServerStr.substr(pServerStr.indexOf(" ")+1);
+}
+
+function testCurrentColorChangeOnFallback()
+{
+ setupTest();
+ gCircle.setAttribute("color", "red"); // At first: currentColor=red
+ var anim = createAnimSetTo("fill", "url(#missingGrad) currentColor");
+
+ gSvg.setCurrentTime(0);
+ var fallback =
+ getFallbackColor(SMILUtil.getComputedStyleSimple(gCircle, "fill"));
+ is(fallback, "rgb(255, 0, 0)",
+ "Checking animated fallback fill=currentColor after animating");
+
+ gCircle.setAttribute("color", "lime"); // Change: currentColor=lime
+ gSvg.setCurrentTime(0);
+ fallback = getFallbackColor(SMILUtil.getComputedStyleSimple(gCircle, "fill"));
+ is(fallback, "rgb(0, 255, 0)",
+ "Checking animated fallback fill=currentColor after updating context");
+
+ gCircle.removeAttribute("style");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testInheritChange()
+{
+ setupTest();
+ gCircleParent.setAttribute("fill", "red"); // At first: inherit=red
+ var anim = createAnimSetTo("fill", "inherit");
+
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)",
+ "Checking animated fill=inherit after animating");
+
+ gCircleParent.setAttribute("fill", "lime"); // Change: inherit=lime
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)",
+ "Checking animated fill=inherit after updating context");
+
+ gCircleParent.removeAttribute("fill");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testInheritChangeUsingStyle()
+{
+ setupTest();
+ gCircleParent.setAttribute("style", "fill: red"); // At first: inherit=red
+ var anim = createAnimSetTo("fill", "inherit");
+
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)",
+ "Checking animated fill=inherit after animating (using style attr)");
+
+ gCircleParent.setAttribute("style", "fill: lime"); // Change: inherit=lime
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)",
+ "Checking animated fill=inherit after updating context "
+ + "(using style attr)");
+
+ gCircleParent.removeAttribute("style");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testEmUnitChangeOnProp()
+{
+ setupTest();
+ gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px
+ var anim = createAnimSetTo("font-size", "2em");
+
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "20px",
+ "Checking animated font-size=2em after animating ends");
+
+ gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "40px",
+ "Checking animated font-size=2em after updating context");
+
+ gCircleParent.removeAttribute("font-size");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testEmUnitChangeOnPropBase()
+{
+ // Test the case where the base value for our animation sandwich is
+ // context-sensitive.
+ // Currently, this is taken care of by the compositor which keeps a cached
+ // base value and compares it with the current base value. This test then just
+ // serves as a regression test in case the compositor's behaviour changes.
+ setupTest();
+ gSvg.setAttribute("font-size", "10px"); // At first: font-size: 10px
+ gCircleParent.setAttribute("font-size", "1em"); // Base: 10px
+ var anim = createAnimBy("font-size", "10px");
+
+ gSvg.setCurrentTime(TIME_AFTER_ANIM_END);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "20px",
+ "Checking animated font-size=20px after anim ends");
+
+ gSvg.setAttribute("font-size", "20px"); // Change: font-size: 20px
+ gSvg.setCurrentTime(TIME_AFTER_ANIM_END);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "30px",
+ "Checking animated font-size=30px after updating context");
+
+ gCircleParent.removeAttribute("font-size");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testEmUnitChangeOnLength()
+{
+ setupTest();
+ gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px
+ var anim = createAnimSetTo("cx", "2em");
+
+ gSvg.setCurrentTime(0);
+ is(gCircle.cx.animVal.value, 20,
+ "Checking animated length=2em after animating");
+
+ gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px
+ // Bug 508206: We should really detect this change and update immediately but
+ // currently we don't until we get sampled again
+ todo_is(gCircle.cx.animVal.value, 40,
+ "Checking animated length=2em after updating context but before sampling");
+
+ gSvg.setCurrentTime(0);
+ is(gCircle.cx.animVal.value, 40,
+ "Checking animated length=2em after updating context and after " +
+ "resampling");
+
+ gCircleParent.removeAttribute("font-size");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testPercentUnitChangeOnProp()
+{
+ setupTest();
+ gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px
+ var anim = createAnimSetTo("font-size", "150%");
+
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "15px",
+ "Checking animated font-size=150% after animating");
+
+ gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px
+ gSvg.setCurrentTime(0);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "30px",
+ "Checking animated font-size=150% after updating context");
+
+ gCircleParent.removeAttribute("font-size");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testPercentUnitChangeOnLength()
+{
+ setupTest();
+ var oldHeight = gSvg.getAttribute("height");
+ gSvg.setAttribute("height", "100px"); // At first: viewport height: 100px
+ var anim = createAnimSetTo("cy", "100%");
+
+ gSvg.setCurrentTime(0); // Force synchronous sample so animation takes effect
+ // Due to bug 627594 (SVGLength.value for percent value lengths doesn't
+ // reflect updated viewport until reflow) the following will fail.
+ // Check that it does indeed fail so that when that bug is fixed this test
+ // can be updated.
+ todo_is(gCircle.cy.animVal.value, 100,
+ "Checking animated length=100% after animating but before reflow");
+ // force a layout flush (Bug 627594)
+ gSvg.getCTM();
+ // Even after doing a reflow though we'll still fail due to bug 508206
+ // (Relative units used in animation don't update immediately)
+ todo_is(gCircle.cy.animVal.value, 100,
+ "Checking animated length=100% after animating but before resampling");
+ gSvg.setCurrentTime(0);
+ // Now we should be up to date
+ is(gCircle.cy.animVal.value, 100,
+ "Checking animated length=100% after animating");
+
+ gSvg.setAttribute("height", "50px"); // Change: height: 50px
+ // force a layout flush (Bug 627594)
+ gSvg.getCTM();
+ gSvg.setCurrentTime(0); // Bug 508206
+ is(gCircle.cy.animVal.value, 50,
+ "Checking animated length=100% after updating context");
+
+ gSvg.setAttribute("height", oldHeight);
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testRelativeFontSize()
+{
+ setupTest();
+ gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px
+ var anim = createAnimSetTo("font-size", "larger");
+
+ gSvg.setCurrentTime(0);
+ var fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size"));
+ // CSS 2 suggests a scaling factor of 1.2 so we should be looking at something
+ // around about 12 or so
+ ok(fsize > 10 && fsize < 20,
+ "Checking animated font-size > 10px after animating");
+
+ gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px
+ gSvg.setCurrentTime(0);
+ fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size"));
+ ok(fsize > 20, "Checking animated font-size > 20px after updating context");
+
+ gCircleParent.removeAttribute("font-size");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testRelativeFontWeight()
+{
+ setupTest();
+ gCircleParent.setAttribute("font-weight", "100"); // At first: font-weight 100
+ var anim = createAnimSetTo("font-weight", "bolder");
+ // CSS 2: 'bolder': Specifies the next weight that is assigned to a font
+ // that is darker than the inherited one. If there is no such weight, it
+ // simply results in the next darker numerical value (and the font remains
+ // unchanged), unless the inherited value was '900', in which case the
+ // resulting weight is also '900'.
+
+ gSvg.setCurrentTime(0);
+ var weight =
+ parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-weight"));
+ ok(weight > 100, "Checking animated font-weight > 100 after animating");
+
+ gCircleParent.setAttribute("font-weight", "800"); // Change: font-weight 800
+ gSvg.setCurrentTime(0);
+ weight = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-weight"));
+ is(weight, 900,
+ "Checking animated font-weight = 900 after updating context");
+
+ gCircleParent.removeAttribute("font-weight");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testRelativeFont()
+{
+ // Test a relative font-size as part of a 'font' spec since the code path
+ // is different in this case
+ // It turns out that, due to the way we store shorthand font properties, we
+ // don't need to worry about marking such values as context-sensitive since we
+ // seem to store them in their relative form. If, however, we change the way
+ // we store shorthand font properties in the future, this will serve as
+ // a useful regression test.
+ setupTest();
+ gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px
+ // We must be sure to set every part of the shorthand property to some
+ // non-context sensitive value because we want to test that even if only the
+ // font-size is relative we will update it appropriately.
+ var anim =
+ createAnimSetTo("font", "normal normal bold larger/normal sans-serif");
+
+ gSvg.setCurrentTime(0);
+ var fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size"));
+ ok(fsize > 10 && fsize < 20,
+ "Checking size of shorthand 'font' > 10px after animating");
+
+ gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px
+ gSvg.setCurrentTime(0);
+ fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size"));
+ ok(fsize > 20,
+ "Checking size of shorthand 'font' > 20px after updating context");
+
+ gCircleParent.removeAttribute("font-size");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testCalcFontSize()
+{
+ setupTest();
+ gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px
+ var anim = createAnimSetTo("font-size", "-moz-calc(110% + 0.1em)");
+
+ gSvg.setCurrentTime(0);
+ var fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size"));
+ // Font size should be 1.1 * 10px + 0.1 * 10px = 12
+ is(fsize, 12, "Checking animated calc font-size == 12px after animating");
+
+ gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px
+ gSvg.setCurrentTime(0);
+ fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size"));
+ is(fsize, 24, "Checking animated calc font-size == 24px after updating " +
+ "context");
+
+ gCircleParent.removeAttribute("font-size");
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testDashArray()
+{
+ // stroke dasharrays don't currently convert units--but if someone ever fixes
+ // that, hopefully this test will fail and remind us not to cache percentage
+ // values in that case
+ setupTest();
+ var oldHeight = gSvg.getAttribute("height");
+ var oldWidth = gSvg.getAttribute("width");
+ gSvg.setAttribute("height", "100px"); // At first: viewport: 100x100px
+ gSvg.setAttribute("width", "100px");
+ var anim = createAnimFromTo("stroke-dasharray", "0 5", "0 50%");
+
+ gSvg.setCurrentTime(TIME_AFTER_ANIM_END);
+
+ // Now we should be up to date
+ is(SMILUtil.getComputedStyleSimple(gCircle, "stroke-dasharray"), "0, 50%",
+ "Checking animated stroke-dasharray after animating");
+
+ gSvg.setAttribute("height", "50px"); // Change viewport: 50x50px
+ gSvg.setAttribute("width", "50px");
+ gSvg.setCurrentTime(TIME_AFTER_ANIM_END);
+ is(SMILUtil.getComputedStyleSimple(gCircle, "stroke-dasharray"), "0, 50%",
+ "Checking animated stroke-dasharray after updating context");
+
+ gSvg.setAttribute("height", oldHeight);
+ gSvg.setAttribute("width", oldWidth);
+ gCircle.removeChild(gCircle.firstChild);
+}
+
+function testClip()
+{
+ setupTest();
+ gCircleParent.setAttribute("font-size", "20px"); // At first: font-size: 20px
+
+ // The clip property only applies to elements that establish a new
+ // viewport so we need to create a nested svg and add animation to that
+ var nestedSVG = document.createElementNS(SVGNS, "svg");
+ nestedSVG.setAttribute("clip", "rect(0px 0px 0px 0px)");
+ gCircleParent.appendChild(nestedSVG);
+
+ var anim = createAnimSetTo("clip", "rect(1em 1em 1em 1em)");
+ // createAnimSetTo will make the animation a child of gCircle so we need to
+ // move it so it targets nestedSVG instead
+ nestedSVG.appendChild(anim);
+
+ gSvg.setCurrentTime(TIME_AFTER_ANIM_END);
+ is(SMILUtil.getComputedStyleSimple(nestedSVG, "clip"),
+ "rect(20px, 20px, 20px, 20px)",
+ "Checking animated clip rect after animating");
+
+ gCircleParent.setAttribute("font-size", "10px"); // Change: font-size: 10px
+ gSvg.setCurrentTime(TIME_AFTER_ANIM_END);
+ is(SMILUtil.getComputedStyleSimple(nestedSVG, "clip"),
+ "rect(10px, 10px, 10px, 10px)",
+ "Checking animated clip rect after updating context");
+
+ gCircleParent.removeAttribute("font-size");
+ gCircleParent.removeChild(nestedSVG);
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test conditional processing tests applied to animations</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle r="50" fill="blue" id="circle">
+ <set attributeName="cy" to="100" begin="0s" dur="100s" id="a"/>
+ <set attributeName="cx" to="100" begin="a.end" dur="100s" id="b"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var svg = document.getElementById("svg"),
+ a = document.getElementById("a"),
+ b = document.getElementById("b"),
+ circle = document.getElementById("circle");
+
+// Check initial state
+svg.setCurrentTime(50);
+is(a.getStartTime(), 0, "a has resolved start time at start");
+is(circle.cy.animVal.value, 100, "a is in effect at start");
+is(b.getStartTime(), 100, "b has resolved start time at start");
+
+// Add a failing conditional processing test
+a.setAttribute("systemLanguage", "no-such-language");
+ok(hasUnresolvedStartTime(a),
+ "a has unresolved start time with failing conditional processing test");
+is(circle.cy.animVal.value, 0,
+ "a is not in effect with failing conditional processing test");
+ok(hasUnresolvedStartTime(b),
+ "b has unresolved start time with failing conditional processing test on a");
+
+// Remove failing conditional processing test
+a.removeAttribute("systemLanguage");
+is(a.getStartTime(), 0, "a has resolved start time after removing test");
+is(circle.cy.animVal.value, 100, "a is in effect after removing test");
+is(b.getStartTime(), 100, "b has resolved start time after removing test on a");
+
+// Add another failing conditional processing test
+// According to the spec, if a null string or empty string value is set for
+// the 'systemLanguage' attribute, the attribute returns "false".
+a.setAttribute("systemLanguage", "");
+
+// Fast forward until |a| would have finished
+var endEventsReceived = 0;
+a.addEventListener("endEvent", function() { endEventsReceived++; });
+svg.setCurrentTime(150);
+is(endEventsReceived, 0,
+ "a does not dispatch end events with failing condition processing test");
+is(circle.cx.animVal.value, 0,
+ "b is not in effect with failing conditional processing test on a");
+
+// Make test pass
+a.setAttribute("systemLanguage", "en");
+is(circle.cx.animVal.value, 100,
+ "b is in effect with passing conditional processing test on a");
+
+function hasUnresolvedStartTime(anim) {
+ // getStartTime throws INVALID_STATE_ERR when there is no current interval
+ try {
+ anim.getStartTime();
+ return false;
+ } catch(e) {
+ return true;
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
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 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for adding and removing animations from a time container</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="-20" cy="20" r="15" fill="blue" id="circle">
+ <set attributeName="cy" to="120" begin="0s; 2s" dur="1s" id="b"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for adding and removing animations from a time container **/
+
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ var svg = getElement("svg");
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ // Create animation and check initial state
+ var anim = createAnim();
+ anim.setAttribute('begin','b.begin+2s; 6s');
+ ok(noStart(anim), "Animation has start time before attaching to document.");
+
+ // Attach animation to container
+ var circle = getElement("circle");
+ circle.appendChild(anim);
+
+ // Check state after attaching
+ is(anim.getStartTime(), 2);
+
+ // Unbind from tree -- the syncbase instance time(s) should become unresolved
+ // but the offset time should remain
+ anim.parentNode.removeChild(anim);
+ is(anim.getStartTime(), 6);
+
+ // Rebind and check everything is re-resolved
+ circle.appendChild(anim);
+ is(anim.getStartTime(), 2);
+
+ // Advance document time to t=1s
+ // Now the current interval for b is 2s-3s but the current interval for anim
+ // is still 2s-2.5s based on b's previous interval
+ svg.setCurrentTime(1);
+ is(anim.getStartTime(), 2);
+
+ // Unbind
+ anim.parentNode.removeChild(anim);
+ is(anim.getStartTime(), 6);
+
+ // Rebind
+ // At this point only the current interval will be re-added to anim (this is
+ // for consistency since old intervals may or may not have been filtered).
+ // Therefore the start time should be 4s instead of 2s.
+ circle.appendChild(anim);
+ is(anim.getStartTime(), 4);
+
+ SimpleTest.finish();
+}
+
+function createAnim() {
+ const svgns="http://www.w3.org/2000/svg";
+ var anim = document.createElementNS(svgns,'set');
+ anim.setAttribute('attributeName','cx');
+ anim.setAttribute('to','100');
+ anim.setAttribute('dur','0.5s');
+ return anim;
+}
+
+function noStart(elem) {
+ var exceptionCaught = false;
+
+ try {
+ elem.getStartTime();
+ } catch(e) {
+ exceptionCaught = true;
+ is (e.name, "InvalidStateError",
+ "Unexpected exception from getStartTime.");
+ is (e.code, DOMException.INVALID_STATE_ERR,
+ "Unexpected exception code from getStartTime.");
+ }
+
+ return exceptionCaught;
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for moving animations between time containers</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svga" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="-20" cy="20" r="15" fill="blue" id="circlea"/>
+</svg>
+<svg id="svgb" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="-20" cy="20" r="15" fill="blue" id="circleb">
+ <set attributeName="cy" to="120" begin="4s" dur="1s" id="syncb"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for moving animations between time containers **/
+
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ var svga = getElement("svga");
+ ok(svga.animationsPaused(), "should be paused by <svg> load handler");
+ is(svga.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+ svga.setCurrentTime(1);
+
+ var svgb = getElement("svgb");
+ ok(svgb.animationsPaused(), "should be paused by <svg> load handler");
+ is(svgb.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+ svgb.setCurrentTime(1);
+
+ // Create animation and check initial state
+ var anim = createAnim();
+ ok(noStart(anim), "Animation has start time before attaching to document");
+
+ // Attach animation to first container
+ var circlea = getElement("circlea");
+ var circleb = getElement("circleb");
+ circlea.appendChild(anim);
+
+ // Check state after attaching
+ is(anim.getStartTime(), 2,
+ "Unexpected start time after attaching animation to target");
+ is(circlea.cx.animVal.value, -20,
+ "Unexpected animated value for yet-to-start animation");
+ is(circleb.cx.animVal.value, -20,
+ "Unexpected animated value for unanimated target");
+
+ // Move animation from first container to second
+ circleb.appendChild(anim);
+
+ // Advance first container and check animation has no effect
+ svga.setCurrentTime(2);
+ is(anim.getStartTime(), 2,
+ "Unexpected start time after moving animation");
+ is(circlea.cx.animVal.value, -20,
+ "Unexpected animated value for non-longer-animated target");
+ is(circleb.cx.animVal.value, -20,
+ "Unexpected animated value for now yet-to-start animation");
+
+ // Advance second container and check the animation only affects it
+ svgb.setCurrentTime(2);
+ is(anim.getStartTime(), 2, "Start time changed after time container seek");
+ is(circlea.cx.animVal.value, -20,
+ "Unanimated target changed after seek on other container");
+ is(circleb.cx.animVal.value, 100, "Animated target not animated after seek");
+
+ // Remove animation so that it belongs to no container and check that
+ // advancing the second container to the next milestone doesn't cause a crash
+ // (when the animation controller goes to run the next milestone sample).
+ anim.parentNode.removeChild(anim);
+ svgb.setCurrentTime(3);
+
+ // Do likewise with syncbase relationships
+
+ // Create the syncbase relationship
+ anim.setAttribute('begin', 'syncb.begin');
+
+ // Attach to second time container (where t=3s)
+ circleb.appendChild(anim);
+ is(anim.getStartTime(), 4,
+ "Unexpected start time for cross-time container syncbase dependency");
+
+ // Move to first time container (where t=1s).
+ // Because we're dealing with different time containers and both are paused,
+ // future times are effectively unresolved.
+ circlea.appendChild(anim);
+ ok(noStart(anim), "Unexpected start time for paused time container");
+
+ SimpleTest.finish();
+}
+
+function createAnim() {
+ const svgns="http://www.w3.org/2000/svg";
+ var anim = document.createElementNS(svgns,'set');
+ anim.setAttribute('attributeName','cx');
+ anim.setAttribute('to','100');
+ anim.setAttribute('begin','2s');
+ anim.setAttribute('dur','1s');
+ return anim;
+}
+
+function noStart(elem) {
+ var exceptionCaught = false;
+
+ try {
+ elem.getStartTime();
+ } catch(e) {
+ exceptionCaught = true;
+ is (e.name, "InvalidStateError",
+ "Unexpected exception from getStartTime.");
+ is (e.code, DOMException.INVALID_STATE_ERR,
+ "Unexpected exception code from getStartTime");
+ }
+
+ return exceptionCaught;
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=699143
+-->
+<head>
+ <title>Test for Bug 699143</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=699143">Mozilla Bug 699143</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+ <svg xmlns="http://www.w3.org/2000/svg">
+ <rect id="r" height="500px" width="500px" fill="blue"/>
+ </svg>
+</div>
+<pre id="test">
+<script type="text/javascript">
+<![CDATA[
+
+/** Test for Bug 699143 **/
+SimpleTest.waitForExplicitFinish();
+
+// Values for 'width' attr on the <rect> above
+const INITIAL_VAL = "500px"
+const FROM_VAL = "20px";
+const TO_VAL = "80px";
+
+// Helper functions
+
+// This function allows 10ms to pass
+function allowTimeToPass() {
+ var initialDate = new Date();
+ while (new Date() - initialDate < 10) {}
+}
+
+// This function returns a newly created <animate> element for use in this test
+function createAnim() {
+ var a = document.createElementNS('http://www.w3.org/2000/svg', 'animate');
+ a.setAttribute('attributeName', 'width');
+ a.setAttribute('from', FROM_VAL);
+ a.setAttribute('to', TO_VAL);
+ a.setAttribute('begin', 'indefinite');
+ a.setAttribute('dur', '3s');
+ a.setAttribute('fill', 'freeze');
+ return a;
+}
+
+// Main Functions
+function main() {
+ // In unpatched Firefox builds, we'll only trigger Bug 699143 if we insert
+ // an animation and call beginElement() **after** the document start-time.
+ // Hence, we use executeSoon here to allow some time to pass. (And then
+ // we'll use a short busy-loop, for good measure.)
+ SimpleTest.executeSoon(runTest);
+}
+
+function runTest() {
+ var svg = SMILUtil.getSVGRoot();
+
+ // In case our executeSoon fired immediately, we force a very small amount
+ // of time to pass here, using a 10ms busy-loop.
+ allowTimeToPass();
+
+ is(svg.getCurrentTime(), 0,
+ "even though we've allowed time to pass, we shouldn't have bothered " +
+ "updating the current time, since there aren't any animation elements");
+
+ // Insert an animation elem (should affect currentTime but not targeted attr)
+ var r = document.getElementById("r");
+ var a = createAnim();
+ r.appendChild(a);
+ isnot(svg.getCurrentTime(), 0,
+ "insertion of first animation element should have triggered a " +
+ "synchronous sample and updated our current time");
+ is(r.width.animVal.valueAsString, INITIAL_VAL,
+ "inserted animation shouldn't have affected its targeted attribute, " +
+ "since it doesn't have any intervals yet");
+
+ // Trigger the animation & be sure it takes effect
+ a.beginElement();
+ is(r.width.animVal.valueAsString, FROM_VAL,
+ "beginElement() should activate our animation & set its 'from' val");
+
+ // Rewind to time=0 & check target attr, to be sure beginElement()-generated
+ // interval starts later than that.
+ svg.setCurrentTime(0);
+ is(r.width.animVal.valueAsString, INITIAL_VAL,
+ "after rewinding to 0, our beginElement()-generated interval " +
+ "shouldn't be active yet");
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=628888
+-->
+<head>
+ <title>Test for Bug 628888 - Animations in external document sometimes don't run</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body style="margin:0px">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=628888">Mozilla Bug 628888</a>
+<p id="display"></p>
+<div id="content" style="background: red; width: 50px; height: 50px"/>
+
+<pre id="test">
+<script type="application/javascript">
+<![CDATA[
+
+/* Test for Bug 628888 - Animations in external document sometimes don't run
+ *
+ * This bug concerns a condition where an external document is loaded after the
+ * page show event is dispatched, leaving the external document paused.
+ *
+ * To reproduce the bug we attach an external document with animation after the
+ * page show event has fired.
+ *
+ * However, it is difficult to test if the animation is playing or not since we
+ * don't receive events from animations running in an external document.
+ *
+ * Our approach is to simply render the result to a canvas (which requires
+ * elevated privileges and that is why we are using a MochiTest rather
+ * than a reftest) and poll one of the pixels to see if it changes colour.
+ *
+ * This should mean the test succeeds quickly but fails slowly.
+ */
+
+const POLL_INTERVAL = 100; // ms
+const POLL_TIMEOUT = 10000; // ms
+var accumulatedWaitTime = 0;
+
+function pageShow()
+{
+ var content = document.getElementById("content");
+ content.style.filter = "url(smilExtDoc_helper.svg#filter)";
+ window.setTimeout(checkResult, 0);
+}
+
+function checkResult()
+{
+ var content = document.getElementById("content");
+ var bbox = content.getBoundingClientRect();
+
+ var canvas = SpecialPowers.snapshotRect(window, bbox);
+ var ctx = canvas.getContext("2d");
+
+ var imgd = ctx.getImageData(bbox.width/2, bbox.height/2, 1, 1);
+ var isGreen = (imgd.data[0] == 0) &&
+ (imgd.data[1] == 255) &&
+ (imgd.data[2] == 0);
+ if (isGreen) {
+ ok(true, "Filter is animated as expected");
+ } else if (accumulatedWaitTime >= POLL_TIMEOUT) {
+ ok(false, "No animation detected after waiting " + POLL_TIMEOUT + "ms");
+ } else {
+ accumulatedWaitTime += POLL_INTERVAL;
+ window.setTimeout(checkResult, POLL_INTERVAL);
+ return;
+ }
+ // Hide our content since mochitests normally try to be visually "quiet"
+ content.style.display = 'none';
+ SimpleTest.finish();
+}
+window.addEventListener('pageshow', pageShow, false);
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL fill modes</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL fill modes **/
+
+/* Global Variables */
+const svgns="http://www.w3.org/2000/svg";
+var svg = document.getElementById("svg");
+var circle = document.getElementById('circle');
+
+SimpleTest.waitForExplicitFinish();
+
+function createAnim() {
+ var anim = document.createElementNS(svgns,'animate');
+ anim.setAttribute('attributeName','cx');
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('begin','0s');
+ anim.setAttribute('values', '10; 20');
+ return circle.appendChild(anim);
+}
+
+function main() {
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var tests =
+ [ testSetLaterA,
+ testSetLaterB,
+ testRemoveLater ];
+ for (var i = 0; i < tests.length; i++) {
+ var anim = createAnim();
+ svg.setCurrentTime(0);
+ tests[i](anim);
+ anim.parentNode.removeChild(anim);
+ }
+ SimpleTest.finish();
+}
+
+function checkSample(time, expectedValue) {
+ svg.setCurrentTime(time);
+ is(circle.cx.animVal.value, expectedValue,
+ "Updated fill mode not applied to animation");
+}
+
+// Test that we can update the fill mode after an interval has played and it
+// will be updated correctly.
+function testSetLaterA(anim) {
+ checkSample(5, -100);
+ anim.setAttribute('fill', 'freeze');
+ is(circle.cx.animVal.value, 20,
+ "Fill not applied for retrospectively set fill mode");
+}
+
+function testSetLaterB(anim) {
+ anim.setAttribute('fill', 'freeze');
+ checkSample(5, 20);
+}
+
+function testRemoveLater(anim) {
+ anim.setAttribute('fill', 'freeze');
+ checkSample(5, 20);
+ anim.setAttribute('fill', 'remove');
+ is(circle.cx.animVal.value, -100,
+ "Fill not removed for retrospectively set fill mode");
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for getSimpleDuration Behavior </title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px">
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" attributeType="XML"
+ from="20" to="100" begin="1s" id="anim"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for getSimpleDuration Behavior **/
+
+/* Global Variables */
+var svg = document.getElementById("svg");
+
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ var anim = document.getElementById("anim");
+
+ /* Check initial state */
+ checkForException(anim, "dur not set");
+
+ /* Check basic operation */
+ anim.setAttribute("dur", "1s");
+ is(anim.getSimpleDuration(), 1);
+ anim.setAttribute("dur", "1.5s");
+ is(anim.getSimpleDuration(), 1.5);
+
+ /* Check exceptional states */
+ anim.setAttribute("dur", "0s");
+ checkForException(anim, "dur=0s");
+ anim.setAttribute("dur", "-1s");
+ checkForException(anim, "dur=-1s");
+ anim.setAttribute("dur", "indefinite");
+ checkForException(anim, "dur=indefinite");
+ anim.setAttribute("dur", "media");
+ checkForException(anim, "dur=media");
+ anim.setAttribute("dur", "abc");
+ checkForException(anim, "dur=abc");
+ anim.removeAttribute("dur");
+ checkForException(anim, "dur not set");
+
+ /* Check range/syntax */
+ anim.setAttribute("dur", "100ms");
+ millisecondCompare(anim.getSimpleDuration(), 0.1);
+ anim.setAttribute("dur", "24h");
+ is(anim.getSimpleDuration(), 60 * 60 * 24);
+
+ SimpleTest.finish();
+}
+
+function millisecondCompare(a, b) {
+ is(Math.round(a * 1000), Math.round(b * 1000));
+}
+
+function checkForException(anim, descr) {
+ var gotException = false;
+ try {
+ var dur = anim.getSimpleDuration();
+ } catch(e) {
+ is (e.name, "NotSupportedError",
+ "Wrong exception from getSimpleDuration");
+ is (e.code, DOMException.NOT_SUPPORTED_ERR,
+ "Wrong exception from getSimpleDuration");
+ gotException = true;
+ }
+ ok(gotException,
+ "Exception not thrown for indefinite simple duration when " + descr);
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for getStartTime Behavior </title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" attributeType="XML"
+ from="20" to="100" begin="indefinite" dur="1s" id="anim"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for getStartTime Behavior **/
+
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ var svg = document.getElementById("svg");
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var anim = document.getElementById("anim");
+ // indefinite
+ var exceptionCaught = false;
+ try {
+ anim.getStartTime();
+ } catch(e) {
+ exceptionCaught = true;
+ is(e.name, "InvalidStateError",
+ "Unexpected exception from getStartTime.");
+ is(e.code, DOMException.INVALID_STATE_ERR,
+ "Unexpected exception code from getStartTime.");
+ }
+ ok(exceptionCaught, "No exception thrown for indefinite start time.");
+
+ // 1s
+ anim.setAttribute("begin", "1s");
+ is(anim.getStartTime(), 1, "Unexpected start time with begin=1s");
+
+ // We have to be careful here when choosing a negative time that we choose
+ // a time that will create an interval that reaches past t=0 as SMIL has
+ // special rules for throwing away intervals that end before t=0
+ anim.setAttribute("begin", "-0.5s");
+ is(anim.getStartTime(), -0.5, "Unexpected start time with begin=-0.5s");
+
+ // Once the animation has begun, the begin time is fixed so we need to end the
+ // element (or advance the timeline) to override the previous start time
+ anim.endElement();
+
+ // However, now we have an end instance, and the SMIL model dictates that if
+ // we have end instances and no end event conditions and all end instances are
+ // before our next begin, there's no valid interval. To overcome this we add
+ // an indefinite end.
+ anim.setAttribute("end", "indefinite");
+
+ // Now test over the lifetime of the animation when there are multiple
+ // intervals
+ anim.setAttribute("begin", "1s; 3s");
+ is(anim.getStartTime(), 1, "Unexpected start time before first interval");
+
+ svg.setCurrentTime(1);
+ is(anim.getStartTime(), 1,
+ "Unexpected start time at start of first interval");
+
+ svg.setCurrentTime(1.5);
+ is(anim.getStartTime(), 1, "Unexpected start time during first interval");
+
+ svg.setCurrentTime(2);
+ is(anim.getStartTime(), 3, "Unexpected start time after first interval");
+
+ svg.setCurrentTime(3);
+ is(anim.getStartTime(), 3, "Unexpected start time during second interval");
+
+ svg.setCurrentTime(4);
+ exceptionCaught = false;
+ try {
+ anim.getStartTime();
+ } catch(e) {
+ exceptionCaught = true;
+ is(e.name, "InvalidStateError",
+ "Unexpected exception from getStartTime.");
+ is(e.code, DOMException.INVALID_STATE_ERR,
+ "Unexpected exception code from getStartTime.");
+ }
+ ok(exceptionCaught, "No exception thrown for in postactive state.");
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for hyperlinking</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display:none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL keySplines **/
+
+/* Global Variables */
+const SVGNS="http://www.w3.org/2000/svg";
+var gSvg = document.getElementById("svg");
+var gAnim;
+
+var gTestStages =
+ [ testActive,
+ testSeekToFirst,
+ testKickStart,
+ testKickStartWithUnresolved,
+ testFiltering
+ ];
+
+SimpleTest.waitForExplicitFinish();
+
+function continueTest()
+{
+ if (gTestStages.length == 0) {
+ SimpleTest.finish();
+ return;
+ }
+
+ window.location.hash = "";
+ if (gAnim) {
+ gAnim.parentNode.removeChild(gAnim);
+ }
+ gAnim = createAnim();
+ gSvg.setCurrentTime(0);
+ gTestStages.shift()();
+}
+
+function createAnim() {
+ var anim = document.createElementNS(SVGNS,'animate');
+ anim.setAttribute('attributeName','cx');
+ anim.setAttribute('from','0');
+ anim.setAttribute('to','100');
+ anim.setAttribute('dur','1s');
+ anim.setAttribute('begin','indefinite');
+ anim.setAttribute('id','anim');
+ return document.getElementById('circle').appendChild(anim);
+}
+
+// Traversing a hyperlink, condition 1:
+//
+// "If the target element is active, seek the document time back to the
+// (current) begin time of the element. If there are multiple begin times, use
+// the begin time that corresponds to the current "begin instance"."
+//
+function testActive() {
+ gAnim.setAttribute('begin','2s; 4s');
+ gSvg.setCurrentTime(2.5);
+ fireLink(rewindActiveInterval1);
+}
+
+function rewindActiveInterval1() {
+ is(gSvg.getCurrentTime(), 2,
+ "Unexpected time after activating link to animation in the middle of " +
+ "first active interval");
+
+ // Seek to second interval
+ gSvg.setCurrentTime(4.5);
+ fireLink(rewindActiveInterval2);
+}
+
+function rewindActiveInterval2() {
+ is(gSvg.getCurrentTime(), 4,
+ "Unexpected time after activating link to animation in the middle of " +
+ "second active interval");
+
+ // Try a negative time
+ gAnim.setAttribute("begin", "-0.5");
+ gSvg.setCurrentTime(0.2);
+ fireLink(rewindActiveIntervalAtZero);
+}
+
+function rewindActiveIntervalAtZero() {
+ is(gSvg.getCurrentTime(), 0,
+ "Unexpected time after activating link to animation in the middle of " +
+ "an active interval that overlaps zero");
+
+ continueTest();
+}
+
+// Traversing a hyperlink, condition 2:
+//
+// "Else if the target element begin time is resolved (i.e., there is any
+// resolved time in the list of begin times, or if the begin time was forced by
+// an earlier hyperlink or a beginElement() method call), seek the document time
+// (forward or back, as needed) to the earliest resolved begin time of the
+// target element. Note that the begin time may be resolved as a result of an
+// earlier hyperlink, DOM or event activation. Once the begin time is resolved,
+// hyperlink traversal always seeks."
+//
+function testSeekToFirst() {
+ // Seek forwards
+ gAnim.setAttribute('begin','2s');
+ gSvg.setCurrentTime(0);
+ fireLink(forwardToInterval1);
+}
+
+function forwardToInterval1() {
+ is(gSvg.getCurrentTime(), 2,
+ "Unexpected time after activating link to animation scheduled to start " +
+ "the future");
+
+ // Seek backwards
+ gSvg.setCurrentTime(3.5);
+ fireLink(backwardToInterval1);
+}
+
+function backwardToInterval1() {
+ is(gSvg.getCurrentTime(), 2,
+ "Unexpected time after activating link to animation that ran in the past");
+
+ // What if the first begin instance is negative?
+ gAnim.setAttribute('begin','-0.5s');
+ gSvg.setCurrentTime(1);
+ fireLink(backwardToZero);
+}
+
+function backwardToZero() {
+ is(gSvg.getCurrentTime(), 0,
+ "Unexpected time after activating link to animation that ran in the " +
+ "past with a negative time");
+
+ continueTest();
+}
+
+// Traversing a hyperlink, condition 3:
+//
+// "Else (animation begin time is unresolved) just resolve the target animation
+// begin time at current document time. Disregard the sync-base or event base of
+// the animation, and do not "back-propagate" any timing logic to resolve the
+// child, but rather treat it as though it were defined with begin="indefinite"
+// and just resolve begin time to the current document time."
+//
+function testKickStart() {
+ gSvg.setCurrentTime(1);
+ fireLink(startedAt1s);
+}
+
+function startedAt1s() {
+ is(gSvg.getCurrentTime(), 1,
+ "Unexpected time after kick-starting animation with indefinite start " +
+ "by hyperlink");
+ is(gAnim.getStartTime(), 1,
+ "Unexpected start time for kick-started animation");
+
+ continueTest();
+}
+
+function testKickStartWithUnresolved() {
+ gAnim.setAttribute("begin", "circle.click");
+ gSvg.setCurrentTime(3);
+ fireLink(startedAt3s);
+}
+
+function startedAt3s() {
+ is(gSvg.getCurrentTime(), 3,
+ "Unexpected time after kick-starting animation with unresolved start " +
+ "by hyperlink");
+ is(gAnim.getStartTime(), 3,
+ "Unexpected start time for kick-started animation with unresolved begin " +
+ "condition");
+
+ continueTest();
+}
+
+function testFiltering() {
+ gAnim.setAttribute('begin','-3s; 1s; 2s; 3s; 4s; 5s; 6s; 7s; 8s; 9s; 10s');
+ gSvg.setCurrentTime(12);
+ fireLink(rewindToFirst);
+}
+
+function rewindToFirst() {
+ is(gSvg.getCurrentTime(), 1,
+ "Unexpected time after triggering animation with a hyperlink after " +
+ "numerous intervals have passed");
+
+ continueTest();
+}
+
+function fireLink(callback) {
+ // First we need to reset the hash because otherwise the redundant hashchange
+ // events will be suppressed
+ if (window.location.hash === '') {
+ fireLinkPart2(callback);
+ } else {
+ window.location.hash = '';
+ window.addEventListener("hashchange",
+ function clearHash() {
+ window.removeEventListener("hashchange", clearHash, false);
+ window.setTimeout(fireLinkPart2, 0, callback);
+ },
+ false);
+ }
+}
+
+function fireLinkPart2(callback) {
+ window.addEventListener("hashchange",
+ function triggerCallback() {
+ window.removeEventListener("hashchange", triggerCallback, false);
+ window.setTimeout(callback, 0);
+ },
+ false);
+ window.location.hash = '#anim';
+}
+
+window.addEventListener("load", continueTest, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=941315
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test invalid values cause the model to be updated (bug 941315)</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=941315">Mozilla Bug 941315</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg width="100%" height="1" onload="this.pauseAnimations()">
+ <rect>
+ <animate id="a" dur="100s"/>
+ <animate id="b" dur="5s" begin="a.end"/>
+ </rect>
+ <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ var a = $('a'),
+ b = $('b');
+
+ // Animation doesn't start until onload
+ SimpleTest.waitForExplicitFinish();
+ window.addEventListener("load", runTests, false);
+
+ // Make testing getStartTime easier
+ SVGAnimationElement.prototype.safeGetStartTime = function() {
+ try {
+ return this.getStartTime();
+ } catch(e) {
+ if (e.name == "InvalidStateError" &&
+ e.code == DOMException.INVALID_STATE_ERR) {
+ return 'none';
+ } else {
+ ok(false, "Unexpected exception: " + e);
+ return null;
+ }
+ }
+ };
+
+ function runTests() {
+ [testSimpleDuration, testMin, testMax, testRepeatDur, testRepeatCount]
+ .forEach(function(test) {
+ is(b.getStartTime(), 100, "initial state before running " + test.name);
+ test();
+ is(b.getStartTime(), 100, "final state after running " + test.name);
+ });
+ SimpleTest.finish();
+ }
+
+ function testSimpleDuration() {
+ // Verify a valid value updates as expected
+ a.setAttribute("dur", "50s");
+ is(b.safeGetStartTime(), 50, "valid simple duration");
+
+ // Check an invalid value also causes the model to be updated
+ a.setAttribute("dur", "abc"); // -> indefinite
+ is(b.safeGetStartTime(), "none", "invalid simple duration");
+
+ // Restore state
+ a.setAttribute("dur", "100s");
+ }
+
+ function testMin() {
+ a.setAttribute("min", "200s");
+ is(b.safeGetStartTime(), 200, "valid min duration");
+
+ a.setAttribute("min", "abc"); // -> indefinite
+ is(b.safeGetStartTime(), 100, "invalid min duration");
+
+ a.removeAttribute("min");
+ }
+
+ function testMax() {
+ a.setAttribute("max", "50s");
+ is(b.safeGetStartTime(), 50, "valid max duration");
+
+ a.setAttribute("max", "abc"); // -> indefinite
+ is(b.safeGetStartTime(), 100, "invalid max duration");
+
+ a.removeAttribute("max");
+ }
+
+ function testRepeatDur() {
+ a.setAttribute("repeatDur", "200s");
+ is(b.safeGetStartTime(), 200, "valid repeatDur duration");
+
+ a.setAttribute("repeatDur", "abc"); // -> indefinite
+ is(b.safeGetStartTime(), 100, "invalid repeatDur duration");
+
+ a.removeAttribute("repeatDur");
+ }
+
+ function testRepeatCount() {
+ a.setAttribute("repeatCount", "2");
+ is(b.safeGetStartTime(), 200, "valid repeatCount duration");
+
+ a.setAttribute("repeatCount", "abc"); // -> indefinite
+ is(b.safeGetStartTime(), 100, "invalid repeatCount duration");
+
+ a.removeAttribute("repeatCount");
+ }
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL keySplines</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL keySplines **/
+
+/* Global Variables */
+const svgns="http://www.w3.org/2000/svg";
+var svg = document.getElementById("svg");
+var circle = document.getElementById('circle');
+
+SimpleTest.waitForExplicitFinish();
+
+function createAnim() {
+ var anim = document.createElementNS(svgns,'animate');
+ anim.setAttribute('attributeName','cx');
+ anim.setAttribute('dur','10s');
+ anim.setAttribute('begin','0s');
+ anim.setAttribute('fill', 'freeze');
+ return circle.appendChild(anim);
+}
+
+function main() {
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var tests =
+ [ testSimpleA, // these first four are from SVG 1.1
+ testSimpleB,
+ testSimpleC,
+ testSimpleD,
+ testSimpleE, // bug 501569
+ testMultipleIntervalsA,
+ testMultipleIntervalsB,
+ testMultipleIntervalsC,
+ testOneValue,
+ testFromTo,
+ testWrongNumSplines,
+ testToAnimation,
+ testOkSyntax,
+ testBadSyntaxA,
+ testBadSyntaxB
+ ];
+ for (var i = 0; i < tests.length; i++) {
+ var anim = createAnim();
+ svg.setCurrentTime(0);
+ tests[i](anim);
+ anim.parentNode.removeChild(anim);
+ }
+ SimpleTest.finish();
+}
+
+function checkSample(time, expectedValue) {
+ svg.setCurrentTime(time);
+ is(circle.cx.animVal.value, expectedValue);
+}
+
+function checkSampleRough(time, expectedValue, precision) {
+ const defaultPrecision = 0.00001;
+ if (typeof precision == "undefined") {
+ precision = defaultPrecision;
+ }
+ svg.setCurrentTime(time);
+ var diff = Math.abs(expectedValue - circle.cx.animVal.value);
+ ok(diff <= precision,
+ "Unexpected sample value got " + circle.cx.animVal.value
+ + ", expected " + expectedValue + " [error is " + diff
+ + ", tolerance is " + precision + "]");
+}
+
+/*
+ * These first four tests are the examples given in SVG 1.1, section 19.2.7
+ */
+
+function testSimpleA(anim) {
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('values', '10; 20');
+ anim.setAttribute('keyTimes', '0; 1');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '0 0 1 1');
+ checkSample(0, 10);
+ checkSample(1, 12.5);
+ checkSample(2, 15);
+ checkSample(3, 17.5);
+ checkSample(4, 20);
+}
+
+function testSimpleB(anim) {
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('values', '10; 20');
+ anim.setAttribute('keyTimes', '0; 1');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '.5 0 .5 1');
+ checkSample(0, 10);
+ checkSampleRough(1, 11.058925);
+ checkSample(2, 15);
+ checkSampleRough(3, 18.941075);
+ checkSample(4, 20);
+}
+
+function testSimpleC(anim) {
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('values', '10; 20');
+ anim.setAttribute('keyTimes', '0; 1');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '0 .75 .25 1');
+ checkSample(0, 10);
+ checkSampleRough(1, 18.101832);
+ checkSampleRough(2, 19.413430);
+ checkSampleRough(3, 19.886504);
+ checkSample(4, 20);
+}
+
+function testSimpleD(anim) {
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('values', '10; 20');
+ anim.setAttribute('keyTimes', '0; 1');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '1 0 .25 .25');
+ checkSample(0, 10);
+ checkSampleRough(1, 10.076925);
+ checkSampleRough(2, 10.644369);
+ checkSampleRough(3, 16.908699);
+ checkSample(4, 20);
+}
+
+// Bug 501569 -- nsSMILKeySpline(1, 0, 0, 1) miscalculates values just under 0.5
+function testSimpleE(anim) {
+ anim.setAttribute('dur','10s');
+ anim.setAttribute('values', '0; 10');
+ anim.setAttribute('keyTimes', '0; 1');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '1 0 0 1');
+ checkSample(0, 0);
+ checkSampleRough(0.001, 0);
+ checkSampleRough(4.95, 3.409174);
+ checkSampleRough(4.98, 3.819443);
+ checkSampleRough(4.99, 4.060174);
+ checkSampleRough(4.999, 4.562510);
+ checkSample(5, 5);
+ checkSampleRough(5.001, 5.437490);
+ checkSampleRough(5.01, 5.939826);
+ checkSampleRough(5.015, 6.075002);
+ checkSampleRough(5.02, 6.180557);
+ checkSampleRough(9.9999, 10);
+ checkSample(10, 10);
+}
+
+function testMultipleIntervalsA(anim) {
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('values', '10; 20; 30');
+ anim.setAttribute('keyTimes', '0; 0.25; 1');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '0 0 1 1; .5 0 .5 1;');
+ checkSample(0.5, 15);
+ checkSampleRough(0.999, 20, 0.02);
+ checkSample(1, 20);
+ checkSampleRough(1.001, 20, 0.05);
+ checkSample(2.5, 25);
+ checkSampleRough(3.25, 29, 0.1);
+}
+
+function testMultipleIntervalsB(anim) {
+ // as above but without keyTimes
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('values', '10; 20; 30');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '0 0 1 1; .5 0 .5 1;');
+ checkSample(1, 15);
+ checkSampleRough(1.999, 20, 0.01);
+ checkSample(2, 20);
+ checkSampleRough(2.001, 20, 0.01);
+ checkSample(3, 25);
+ checkSampleRough(3.5, 29, 0.1);
+}
+
+function testMultipleIntervalsC(anim) {
+ // test some unusual (but valid) syntax
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('values', '10; 20; 30');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', ' 0 .75 0.25 1 ; 1, 0 ,.25 .25 \t');
+ checkSampleRough(3.5, 26.9, 0.2);
+}
+
+function testOneValue(anim) {
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('values', '5');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '0 0 1 1');
+ checkSample(0, 5);
+ checkSample(1.5, 5);
+}
+
+function testFromTo(anim) {
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('from', '10');
+ anim.setAttribute('to', '20');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '.5 0 .5 1');
+ checkSample(0, 10);
+ checkSampleRough(1, 11, 0.1);
+}
+
+function testWrongNumSplines(anim) {
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('from', '10');
+ anim.setAttribute('to', '20');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '.5 0 .5 1; 0 0 1 1');
+ // animation is in error, should do nothing
+ checkSample(1.5, -100);
+}
+
+function testToAnimation(anim) {
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('to', '20');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', '.5 0 .5 1');
+ checkSample(0, -100);
+ checkSampleRough(1, -87.3, 0.1);
+}
+
+function testOkSyntax(anim) {
+ var okStrs = ['0,0,0,0', // all commas
+ ' 0 0 , 0 ,0 ', // mix of separators
+ '0 0 0 0;', // trailing semi-colon
+ '0 0 0 0 ;']; // " "
+
+ for (var i = 0; i < okStrs.length; i++) {
+ testAnim = createAnim();
+ testAnim.setAttribute('dur','4s');
+ testAnim.setAttribute('from', '0');
+ testAnim.setAttribute('to', '20');
+ testAnim.setAttribute('calcMode', 'spline');
+ testAnim.setAttribute('keySplines', okStrs[i]);
+ checkSample(4, 20);
+ testAnim.parentNode.removeChild(testAnim);
+ }
+}
+
+function testBadSyntaxA(anim) {
+ var badStrs = ['', // empty
+ ' ', // whitespace only
+ '0,1.1,0,0', // bad range
+ '0,0,0,-0.1', // " "
+ ' 0 0 , 0 0 ,', // stray comma
+ '1-1 0 0', // path-style separators
+ '0 0 0', // wrong number of values
+ '0 0 0 0 0', // " "
+ '0 0 0 0 0 0 0 0', // " "
+ '0 0 0; 0 0 0 0', // " "
+ '0 0 0; 0', // mis-placed semi-colon
+ ';0 0 0 0']; // " "
+
+ for (var i = 0; i < badStrs.length; i++) {
+ testAnim = createAnim();
+ testAnim.setAttribute('dur','4s');
+ testAnim.setAttribute('from', '0');
+ testAnim.setAttribute('to', '20');
+ testAnim.setAttribute('calcMode', 'spline');
+ testAnim.setAttribute('keySplines', badStrs[i]);
+ checkSample(4, -100);
+ testAnim.parentNode.removeChild(testAnim);
+ }
+}
+
+function testBadSyntaxB(anim) {
+ // test some illegal syntax
+ anim.setAttribute('dur','4s');
+ anim.setAttribute('values', '10; 20; 30');
+ anim.setAttribute('calcMode', 'spline');
+ anim.setAttribute('keySplines', ' 0 .75 0.25 1 ; 1, A0 ,.25 .25 \t');
+ // animation is in error, should do nothing
+ checkSample(3.5, -100);
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL keyTimes</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=557885">Mozilla Bug
+ 557885</a>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px">
+ <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL keyTimes **/
+
+var gSvg = document.getElementById("svg");
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ gSvg.pauseAnimations();
+
+ var testCases = Array();
+
+ // Simple case
+ testCases.push({
+ 'attr' : { 'values': '0; 50; 100',
+ 'keyTimes': '0; .8; 1' },
+ 'times': [ [ 4, 25 ],
+ [ 8, 50 ],
+ [ 9, 75 ],
+ [ 10, 100 ] ]
+ });
+
+ // Parsing tests
+ testCases.push(parseOk(' 0 ; .8;1 ')); // extra whitespace
+ testCases.push(parseNotOk(';0; .8; 1')); // leading semi-colon
+ testCases.push(parseNotOk('; .8; 1')); // leading semi-colon
+ testCases.push(parseOk('0; .8; 1;')); // trailing semi-colon
+ testCases.push(parseNotOk('')); // empty string
+ testCases.push(parseNotOk(' ')); // empty string
+ testCases.push(parseNotOk('0; .8')); // too few values
+ testCases.push(parseNotOk('0; .8; .9; 1')); // too many values
+ testCases.push(parseNotOk('0; 1; .8')); // non-increasing
+ testCases.push(parseNotOk('0; .8; .9')); // final value non-1 with
+ // calcMode=linear
+ testCases.push(parseOk('0; .8; .9', { 'calcMode': 'discrete' }));
+ testCases.push(parseNotOk('0.01; .8; 1')); // first value not 0
+ testCases.push(parseNotOk('0.01; .8; 1', { 'calcMode': 'discrete' }));
+ // first value not 0
+ testCases.push(parseNotOk('0; .8; 1.1')); // out of range
+ testCases.push(parseNotOk('-0.1; .8; 1')); // out of range
+
+
+ // 2 values
+ testCases.push({
+ 'attr' : { 'values': '0; 50',
+ 'keyTimes': '0; 1' },
+ 'times': [ [ 6, 30 ] ]
+ });
+
+ // 1 value
+ testCases.push({
+ 'attr' : { 'values': '50',
+ 'keyTimes': ' 0' },
+ 'times': [ [ 7, 50 ] ]
+ });
+
+ // 1 bad value
+ testCases.push({
+ 'attr' : { 'values': '50',
+ 'keyTimes': '0.1' },
+ 'times': [ [ 0, -100 ] ]
+ });
+
+ // 1 value, calcMode=discrete
+ testCases.push({
+ 'attr' : { 'values': '50',
+ 'calcMode': 'discrete',
+ 'keyTimes': ' 0' },
+ 'times': [ [ 7, 50 ] ]
+ });
+
+ // 1 bad value, calcMode=discrete
+ testCases.push({
+ 'attr' : { 'values': '50',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0.1' },
+ 'times': [ [ 0, -100 ] ]
+ });
+
+ // from-to
+ testCases.push({
+ 'attr' : { 'from': '10',
+ 'to': '20',
+ 'keyTimes': '0.0; 1.0' },
+ 'times': [ [ 3.5, 13.5 ] ]
+ });
+
+ // from-to calcMode=discrete
+ testCases.push({
+ 'attr' : { 'from': '10',
+ 'to': '20',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0.0; 0.7' },
+ 'times': [ [ 0, 10 ],
+ [ 6.9, 10 ],
+ [ 7.0, 20 ],
+ [ 10.0, 20 ],
+ [ 11.0, 20 ] ]
+ });
+
+ // from-to calcMode=discrete one keyTime only
+ testCases.push({
+ 'attr' : { 'values': '20',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0' },
+ 'times': [ [ 0, 20 ],
+ [ 6.9, 20 ],
+ [ 7.0, 20 ],
+ [ 10.0, 20 ],
+ [ 11.0, 20 ] ]
+ });
+
+ // from-to calcMode=discrete one keyTime, mismatches no. values
+ testCases.push({
+ 'attr' : { 'values': '10; 20',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0' },
+ 'times': [ [ 0, -100 ] ]
+ });
+
+ // to
+ testCases.push({
+ 'attr' : { 'to': '100',
+ 'keyTimes': '0.0; 1.0' },
+ 'times': [ [ 0, -100 ],
+ [ 7, 40 ] ]
+ });
+
+ // to -- bad number of keyTimes (too many)
+ testCases.push({
+ 'attr' : { 'to': '100',
+ 'keyTimes': '0.0; 0.5; 1.0' },
+ 'times': [ [ 2, -100 ] ]
+ });
+
+ // unfrozen to calcMode=discrete two keyTimes
+ testCases.push({
+ 'attr' : { 'to': '100',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0.0; 1.0',
+ 'fill': 'remove' },
+ 'times': [ [ 0, -100 ],
+ [ 7, -100 ],
+ [ 10, -100 ],
+ [ 12, -100 ]]
+ });
+
+ // frozen to calcMode=discrete two keyTimes
+ testCases.push({
+ 'attr' : { 'to': '100',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0.0; 1.0' },
+ 'times': [ [ 0, -100 ],
+ [ 7, -100 ],
+ [ 10, 100 ],
+ [ 12, 100 ] ]
+ });
+
+ // to calcMode=discrete -- bad number of keyTimes (one, expecting two)
+ testCases.push({
+ 'attr' : { 'to': '100',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0' },
+ 'times': [ [ 0, -100 ],
+ [ 7, -100 ] ]
+ });
+
+ // values calcMode=discrete
+ testCases.push({
+ 'attr' : { 'values': '0; 10; 20; 30',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0;.2;.4;.6' },
+ 'times': [ [ 0, 0 ],
+ [ 1.9, 0 ],
+ [ 2, 10 ],
+ [ 3.9, 10 ],
+ [ 4.0, 20 ],
+ [ 5.9, 20 ],
+ [ 6.0, 30 ],
+ [ 9.9, 30 ],
+ [ 10.0, 30 ] ]
+ });
+
+ // The following two accumulate tests are from SMIL 3.0
+ // (Note that this behaviour differs from that defined for SVG Tiny 1.2 which
+ // specifically excludes the last value: "Note that in the case of discrete
+ // animation, the frozen value that is used is the value of the animation just
+ // before the end of the active duration.")
+ // accumulate=none
+ testCases.push({
+ 'attr' : { 'values': '0; 10; 20',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0;.5;1',
+ 'fill': 'freeze',
+ 'repeatCount': '2',
+ 'accumulate': 'none' },
+ 'times': [ [ 0, 0 ],
+ [ 5, 10 ],
+ [ 10, 0 ],
+ [ 15, 10 ],
+ [ 20, 20 ],
+ [ 25, 20 ] ]
+ });
+
+ // accumulate=sum
+ testCases.push({
+ 'attr' : { 'values': '0; 10; 20',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0;.5;1',
+ 'fill': 'freeze',
+ 'repeatCount': '2',
+ 'accumulate': 'sum' },
+ 'times': [ [ 0, 0 ],
+ [ 5, 10 ],
+ [ 10, 20 ],
+ [ 15, 30 ],
+ [ 20, 40 ],
+ [ 25, 40 ] ]
+ });
+
+ // If the interpolation mode is paced, the keyTimes attribute is ignored.
+ testCases.push({
+ 'attr' : { 'values': '0; 10; 20',
+ 'calcMode': 'paced',
+ 'keyTimes': '0;.2;1' },
+ 'times': [ [ 0, 0 ],
+ [ 2, 4 ],
+ [ 5, 10 ] ]
+ });
+
+ // SMIL 3 has:
+ // If the simple duration is indefinite and the interpolation mode is
+ // linear or spline, any keyTimes specification will be ignored.
+ // However, since keyTimes represent "a proportional offset into the simple
+ // duration of the animation element" surely discrete animation too cannot use
+ // keyTimes when the simple duration is indefinite. Hence SVGT 1.2 is surely
+ // more correct when it has:
+ // If the simple duration is indefinite, any 'keyTimes' specification will
+ // be ignored.
+ // (linear)
+ testCases.push({
+ 'attr' : { 'values': '0; 10; 20',
+ 'dur': 'indefinite',
+ 'keyTimes': '0;.2;1' },
+ 'times': [ [ 0, 0 ],
+ [ 5, 0 ] ]
+ });
+ // (spline)
+ testCases.push({
+ 'attr' : { 'values': '0; 10; 20',
+ 'dur': 'indefinite',
+ 'calcMode': 'spline',
+ 'keyTimes': '0;.2;1',
+ 'keySplines': '0 0 1 1; 0 0 1 1' },
+ 'times': [ [ 0, 0 ],
+ [ 5, 0 ] ]
+ });
+ // (discrete)
+ testCases.push({
+ 'attr' : { 'values': '0; 10; 20',
+ 'dur': 'indefinite',
+ 'calcMode': 'discrete',
+ 'keyTimes': '0;.2;1' },
+ 'times': [ [ 0, 0 ],
+ [ 5, 0 ] ]
+ });
+
+ for (var i = 0; i < testCases.length; i++) {
+ gSvg.setCurrentTime(0);
+ var test = testCases[i];
+
+ // Create animation elements
+ var anim = createAnim(test.attr);
+
+ // Run samples
+ for (var j = 0; j < test.times.length; j++) {
+ var times = test.times[j];
+ gSvg.setCurrentTime(times[0]);
+ checkSample(anim, times[1], times[0], i);
+ }
+
+ anim.parentNode.removeChild(anim);
+ }
+
+ // fallback to discrete for non-additive animation
+ var attr = { 'values': 'butt; round; square',
+ 'attributeName': 'stroke-linecap',
+ 'calcMode': 'linear',
+ 'keyTimes': '0;.2;1',
+ 'fill': 'remove' };
+ var anim = createAnim(attr);
+ var samples = [ [ 0, 'butt' ],
+ [ 1.9, 'butt' ],
+ [ 2.0, 'round' ],
+ [ 9.9, 'round' ],
+ [ 10, 'butt' ] // fill=remove so we'll never set it to square
+ ];
+ for (var i = 0; i < samples.length; i++) {
+ var sample = samples[i];
+ gSvg.setCurrentTime(sample[0]);
+ checkLineCapSample(anim, sample[1], sample[0],
+ "[non-interpolatable fallback]");
+ }
+ anim.parentNode.removeChild(anim);
+
+ SimpleTest.finish();
+}
+
+function parseOk(str, extra)
+{
+ var attr = { 'values': '0; 50; 100',
+ 'keyTimes': str };
+ if (typeof(extra) == "object") {
+ for (name in extra) {
+ attr[name] = extra[name];
+ }
+ }
+ return {
+ 'attr' : attr,
+ 'times': [ [ 0, 0 ] ]
+ };
+}
+
+function parseNotOk(str, extra)
+{
+ var result = parseOk(str, extra);
+ result.times = [ [ 0, -100 ] ];
+ return result;
+}
+
+function createAnim(attr)
+{
+ const svgns = "http://www.w3.org/2000/svg";
+ var anim = document.createElementNS(svgns, 'animate');
+ anim.setAttribute('attributeName','cx');
+ anim.setAttribute('dur','10s');
+ anim.setAttribute('begin','0s');
+ anim.setAttribute('fill','freeze');
+ for (name in attr) {
+ anim.setAttribute(name, attr[name]);
+ }
+ return document.getElementById('circle').appendChild(anim);
+}
+
+function checkSample(anim, expectedValue, sampleTime, caseNum)
+{
+ var msg = "Test case " + caseNum +
+ " (keyTimes: '" + anim.getAttribute('keyTimes') + "'" +
+ " calcMode: " + anim.getAttribute('calcMode') + "), " +
+ "t=" + sampleTime +
+ ": Unexpected sample value:";
+ is(anim.targetElement.cx.animVal.value, expectedValue, msg);
+}
+
+function checkLineCapSample(anim, expectedValue, sampleTime, caseDescr)
+{
+ var msg = "Test case " + caseDescr +
+ " (keyTimes: '" + anim.getAttribute('keyTimes') + "'" +
+ " calcMode: " + anim.getAttribute('calcMode') + "), " +
+ "t=" + sampleTime +
+ ": Unexpected sample value:";
+ var actualValue =
+ window.getComputedStyle(anim.targetElement, null).
+ getPropertyValue('stroke-linecap');
+ is(actualValue, expectedValue, msg);
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Tests updated intervals</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=555026">Mozilla Bug 555026</a>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle r="10" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test that we ignore keyTimes attr when calcMode="paced" **/
+
+/* Global Variables */
+const SVGNS = "http://www.w3.org/2000/svg";
+const ANIM_DUR = "2s";
+const HALF_TIME = "1";
+const ATTR_NAME = "cx"
+const KEYTIMES_TO_TEST = [
+ // potentially-valid values (depending on number of values in animation)
+ "0; 0.2; 1",
+ "0; 0.5",
+ "0; 1",
+ // invalid values:
+ "", "abc", "-0.5", "0; 0.5; 1.01", "5"
+];
+const gSvg = document.getElementById("svg");
+const gCircle = document.getElementById("circle");
+
+SimpleTest.waitForExplicitFinish();
+
+
+// MAIN FUNCTIONS
+function main() {
+ ok(gSvg.animationsPaused(), "should be paused by <svg> load handler");
+ is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ testByAnimation();
+ testToAnimation();
+ testValuesAnimation();
+ SimpleTest.finish();
+}
+
+function testByAnimation() {
+ for (var i = 0; i < KEYTIMES_TO_TEST.length; i++) {
+ setupTest();
+ var anim = createAnim();
+ anim.setAttribute("by", "200");
+ var curKeyTimes = KEYTIMES_TO_TEST[i];
+ anim.setAttribute("keyTimes", curKeyTimes);
+
+ gSvg.setCurrentTime(HALF_TIME);
+ is(gCircle.cx.animVal.value, 100,
+ "Checking animVal with 'by' and keyTimes='" + curKeyTimes + "'");
+
+ anim.parentNode.removeChild(anim); // clean up
+ }
+}
+
+function testToAnimation() {
+ for (var i = 0; i < KEYTIMES_TO_TEST.length; i++) {
+ setupTest();
+ var anim = createAnim();
+ anim.setAttribute("to", "200");
+ var curKeyTimes = KEYTIMES_TO_TEST[i];
+ anim.setAttribute("keyTimes", curKeyTimes);
+
+ gSvg.setCurrentTime(HALF_TIME);
+ is(gCircle.cx.animVal.value, 100,
+ "Checking animVal with 'to' and keyTimes='" + curKeyTimes + "'");
+
+ anim.parentNode.removeChild(anim); // clean up
+ }
+}
+
+function testValuesAnimation() {
+ for (var i = 0; i < KEYTIMES_TO_TEST.length; i++) {
+ setupTest();
+ var anim = createAnim();
+ anim.setAttribute("values", "100; 110; 200");
+ var curKeyTimes = KEYTIMES_TO_TEST[i];
+ anim.setAttribute("keyTimes", curKeyTimes);
+
+ gSvg.setCurrentTime(HALF_TIME);
+ is(gCircle.cx.animVal.value, 150,
+ "Checking animVal with 'values' and keyTimes='" + curKeyTimes + "'");
+
+ anim.parentNode.removeChild(anim); // clean up
+ }
+}
+
+// HELPER FUNCTIONS
+// Common setup code for each test function: seek to 0, and make sure
+// the previous test cleaned up its animations.
+function setupTest() {
+ gSvg.setCurrentTime(0);
+ if (gCircle.firstChild) {
+ ok(false, "Previous test didn't clean up after itself.");
+ }
+}
+
+function createAnim() {
+ var anim = document.createElementNS(SVGNS,"animate");
+ anim.setAttribute("attributeName", ATTR_NAME);
+ anim.setAttribute("dur", ANIM_DUR);
+ anim.setAttribute("begin", "0s");
+ anim.setAttribute("calcMode", "paced");
+ return gCircle.appendChild(anim);
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Animation Behavior on CSS Properties</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <script type="text/javascript" src="db_smilMappedAttrList.js"></script>
+ <script type="text/javascript" src="db_smilCSSPropertyList.js"></script>
+ <script type="text/javascript" src="db_smilCSSFromBy.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)"
+ onload="this.pauseAnimations()">
+ <rect x="20" y="20" width="200" height="200"/>
+ <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is
+ more predictable. (otherwise, line-height varies depending on platform)
+ -->
+ <text x="20" y="20" style="line-height: 10px !important">testing 123</text>
+ <line/>
+ <marker/>
+ <filter><feDiffuseLighting/></filter>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var testBundles = convertCSSBundlesToMappedAttr(gFromByBundles);
+ testBundleList(testBundles, new SMILTimingData(1.0, 1.0));
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Animation Behavior on CSS Properties</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <script type="text/javascript" src="db_smilMappedAttrList.js"></script>
+ <script type="text/javascript" src="db_smilCSSPropertyList.js"></script>
+ <script type="text/javascript" src="db_smilCSSFromTo.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)"
+ onload="this.pauseAnimations()">
+ <rect x="20" y="20" width="200" height="200"/>
+ <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is
+ more predictable. (otherwise, line-height varies depending on platform)
+ -->
+ <text x="20" y="20">testing 123</text>
+ <line/>
+ <image/>
+ <marker/>
+ <clipPath><circle/></clipPath>
+ <filter><feFlood/></filter>
+ <filter><feDiffuseLighting/></filter>
+ <linearGradient><stop/></linearGradient>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+function checkForUntestedAttributes(bundleList)
+{
+ // Create the set of all the attributes we know about
+ var attributeSet = {};
+ for (attributeLabel in gMappedAttrList) {
+ // insert attribute
+ attributeSet[gMappedAttrList[attributeLabel].attrName] = null;
+ }
+ // Remove tested properties from the set
+ for (var bundleIdx in bundleList) {
+ var bundle = bundleList[bundleIdx];
+ delete attributeSet[bundle.animatedAttribute.attrName];
+ }
+ // Warn about remaining (untested) properties
+ for (var untestedProp in attributeSet) {
+ ok(false, "No tests for attribute '" + untestedProp + "'");
+ }
+}
+
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var testBundles = convertCSSBundlesToMappedAttr(gFromToBundles);
+
+ // FIRST: Warn about any attributes that are missing tests
+ checkForUntestedAttributes(testBundles);
+
+ // Run the actual tests
+ testBundleList(testBundles, new SMILTimingData(1.0, 1.0));
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for Animation Behavior on CSS Properties</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <script type="text/javascript" src="db_smilMappedAttrList.js"></script>
+ <script type="text/javascript" src="db_smilCSSPropertyList.js"></script>
+ <script type="text/javascript" src="db_smilCSSPaced.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg xmlns="http://www.w3.org/2000/svg"
+ width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)"
+ onload="this.pauseAnimations()">
+ <rect x="20" y="20" width="200" height="200"/>
+ <text x="20" y="20">testing 123</text>
+ <marker/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ // Start out with document paused
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var testBundles = convertCSSBundlesToMappedAttr(gPacedBundles);
+ testBundleList(testBundles, new SMILTimingData(1.0, 6.0));
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=948245
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 948245</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=948245">Mozilla Bug 948245</a>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" onload="this.pauseAnimations()">
+ <rect fill="red" id="rect" x="0">
+ <animate attributeName="x" to="100" id="animation" dur="100s" min="200s"/>
+ </rect>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ // The 'min' attribute introduces a kind of additional state into the SMIL
+ // model. If the 'min' attribute extends the active duration, the additional
+ // time between the amount of time the animation normally runs for (called the
+ // 'repeat duration') and the extended active duration is filled using the
+ // fill mode.
+ //
+ // Below we refer to this period of time between the end of the repeat
+ // duration and the end of the active duration as the 'extended period'.
+ //
+ // This test verifies that as we jump in and out of these states we produce
+ // the correct values.
+ //
+ // The test animation above produces an active interval that is longer than
+ // the 'repeating duration' of the animation.
+ var rect = $('rect'),
+ animation = $('animation');
+
+ // Animation doesn't start until onload
+ SimpleTest.waitForExplicitFinish();
+ window.addEventListener("load", runTests, false);
+
+ function runTests() {
+ ok($('svg').animationsPaused(), "should be paused by <svg> load handler");
+
+ // In the extended period (t=150s) we should not be animating or filling
+ // since the default fill mode is "none".
+ animation.ownerSVGElement.setCurrentTime(150);
+ is(rect.x.animVal.value, 0,
+ "Shouldn't fill in extended period with fill='none'");
+
+ // If we set the fill mode we should start filling.
+ animation.setAttribute("fill", "freeze");
+ is(rect.x.animVal.value, 100,
+ "Should fill in extended period with fill='freeze'");
+
+ // If we unset the fill attribute we should stop filling.
+ animation.removeAttribute("fill");
+ is(rect.x.animVal.value, 0, "Shouldn't fill after unsetting fill");
+
+ // If we jump back into the repeated interval (at t=50s) we should be
+ // animating.
+ animation.ownerSVGElement.setCurrentTime(50);
+ is(rect.x.animVal.value, 50, "Should be active in repeating interval");
+
+ // If we jump to the boundary at the start of the extended period we should
+ // not be filling (since we removed the fill attribute above).
+ animation.ownerSVGElement.setCurrentTime(100);
+ is(rect.x.animVal.value, 0,
+ "Shouldn't fill after seeking to boundary of extended period");
+
+ // If we apply a fill mode at this boundary point we should do regular fill
+ // behavior of using the last value in the interpolation range.
+ animation.setAttribute("fill", "freeze");
+ is(rect.x.animVal.value, 100,
+ "Should fill at boundary to extended period");
+
+ // Check that if we seek past the interval we fill with the value at the end
+ // of the _repeat_duration_ not the value at the end of the
+ // _active_duration_.
+ animation.setAttribute("repeatCount", "1.5");
+ animation.ownerSVGElement.setCurrentTime(225);
+ is(rect.x.animVal.value, 50,
+ "Should fill with the end of the repeat duration value");
+
+ SimpleTest.finish();
+ }
+</script>
+</pre>
+</body>
+</html>
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 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=948245
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for repeat duration calculation (Bug 948245)</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank"
+href="https://bugzilla.mozilla.org/show_bug.cgi?id=948245">Mozilla Bug 948245</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" onload="this.pauseAnimations()">
+ <rect>
+ <animate id="a"/>
+ <animate id="b" begin="a.end"/>
+ </rect>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ // Tests the calculation of the repeat duration which is one of the steps
+ // towards determining the active duration.
+ //
+ // The repeat duration is determined by the following three attributes:
+ //
+ // dur: may be definite (e.g. '2s') or 'indefinite' (the default)
+ // repeatCount: may be definite (e.g. '2.5'), 'indefinite', or not set
+ // repeatDur: may be definite (e.g. '5s'), 'indefinite', or not set
+ //
+ // That leaves 18 combinations to test.
+ var testCases =
+ [
+ // 1. repeatDur: definite, repeatCount: definite, dur: definite
+ // (Two test cases here to ensure we get the minimum)
+ { repeatDur: 15, repeatCount: 2, dur: 10, result: 15 },
+ { repeatDur: 25, repeatCount: 2, dur: 10, result: 20 },
+ // 2. repeatDur: indefinite, repeatCount: definite, dur: definite
+ { repeatDur: 'indefinite', repeatCount: 2, dur: 10, result: 20 },
+ // 3. repeatDur: not set, repeatCount: definite, dur: definite
+ { repeatCount: 2, dur: 10, result: 20 },
+ // 4. repeatDur: definite, repeatCount: indefinite, dur: definite
+ { repeatDur: 15, repeatCount: 'indefinite', dur: 10, result: 15 },
+ // 5. repeatDur: indefinite, repeatCount: indefinite, dur: definite
+ { repeatDur: 'indefinite', repeatCount: 'indefinite', dur: 10,
+ result: 'indefinite' },
+ // 6. repeatDur: not set, repeatCount: indefinite, dur: definite
+ { repeatCount: 'indefinite', dur: 10, result: 'indefinite' },
+ // 7. repeatDur: definite, repeatCount: not set, dur: definite
+ { repeatDur: 15, dur: 10, result: 15 },
+ // 8. repeatDur: indefinite, repeatCount: not set, dur: definite
+ { repeatDur: 'indefinite', dur: 10, result: 'indefinite' },
+ // 9. repeatDur: not set, repeatCount: not set, dur: definite
+ { dur: 10, result: 10 },
+ // 10. repeatDur: definite, repeatCount: definite, dur: indefinite
+ { repeatDur: 15, repeatCount: 2, dur: 'indefinite', result: 15 },
+ // 11. repeatDur: indefinite, repeatCount: definite, dur: indefinite
+ { repeatDur: 'indefinite', repeatCount: 2, dur: 'indefinite',
+ result: 'indefinite' },
+ // 12. repeatDur: not set, repeatCount: definite, dur: indefinite
+ { repeatCount: 2, dur: 'indefinite', result: 'indefinite' },
+ // 13. repeatDur: definite, repeatCount: indefinite, dur: indefinite
+ { repeatDur: 15, repeatCount: 'indefinite', dur: 'indefinite',
+ result: 15 },
+ // 14. repeatDur: indefinite, repeatCount: indefinite, dur: indefinite
+ { repeatDur: 'indefinite', repeatCount: 'indefinite', dur: 'indefinite',
+ result: 'indefinite' },
+ // 15. repeatDur: not set, repeatCount: indefinite, dur: indefinite
+ { repeatCount: 'indefinite', dur: 'indefinite', result: 'indefinite' },
+ // 16. repeatDur: definite, repeatCount: not set, dur: indefinite
+ { repeatDur: 15, dur: 'indefinite', result: 15 },
+ // 17. repeatDur: indefinite, repeatCount: not set, dur: indefinite
+ { repeatDur: 'indefinite', dur: 'indefinite', result: 'indefinite' },
+ // 18. repeatDur: not set, repeatCount: not set, dur: indefinite
+ { dur: 'indefinite', result: 'indefinite' }
+ ];
+
+ // We can test the repeat duration by setting these attributes on animation
+ // 'a' and checking the start time of 'b' which is defined to start when 'a'
+ // finishes.
+ //
+ // Since 'a' has no end/min/max attributes the end of its active interval
+ // should coincide with the end of its repeat duration.
+ //
+ // Sometimes the repeat duration is defined to be 'indefinite'. In this case
+ // calling getStartTime on b will throw an exception so we need to catch that
+ // exception and translate it to 'indefinite' as follows:
+ function getRepeatDuration() {
+ try {
+ return $('b').getStartTime();
+ } catch(e) {
+ if (e.name == "InvalidStateError" &&
+ e.code == DOMException.INVALID_STATE_ERR) {
+ return 'indefinite';
+ } else {
+ ok(false, "Unexpected exception: " + e);
+ return null;
+ }
+ }
+ }
+
+ // Animation doesn't start until onload
+ SimpleTest.waitForExplicitFinish();
+ window.addEventListener("load", runTests, false);
+
+ // Run through each of the test cases
+ function runTests() {
+ ok($('svg').animationsPaused(), "should be paused by <svg> load handler");
+
+ testCases.forEach(function(test) {
+ var a = $('a');
+
+ // Set the attributes
+ var msgPieces = [];
+ [ 'repeatDur', 'repeatCount', 'dur' ].forEach(function(attr) {
+ if (typeof test[attr] != "undefined") {
+ a.setAttribute(attr, test[attr].toString());
+ msgPieces.push(attr + ': ' + test[attr].toString());
+ } else {
+ a.removeAttribute(attr);
+ msgPieces.push(attr + ': <not set>');
+ }
+ });
+ var msg = msgPieces.join(', ');
+
+ // Check the result
+ is(getRepeatDuration(), test.result, msg);
+ });
+
+ SimpleTest.finish();
+ }
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=485157
+-->
+<head>
+ <title>Test repeat timing</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=485157">Mozilla Bug
+ 485157</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="100px" height="100px">
+ <rect width="100" height="100" fill="green">
+ <set attributeName="width" to="100" dur="20s" repeatCount="5" begin="0s"
+ id="a" onrepeat="startWaiting(evt)"/>
+ <set attributeName="fill" attributeType="CSS" to="green"
+ begin="a.repeat(1)" onbegin="expectedBegin()" dur="20s"/>
+ <set attributeName="x" to="100"
+ begin="a.repeat(2)" onbegin="unexpectedBegin(this)" dur="20s"/>
+ <set attributeName="y" to="100"
+ begin="a.repeat(0)" onbegin="unexpectedBegin(this)" dur="20s"/>
+ <set attributeName="width" to="100"
+ begin="a.repeat(-1)" onbegin="unexpectedBegin(this)" dur="20s"/>
+ <set attributeName="height" to="100"
+ begin="a.repeat(a)" onbegin="unexpectedBegin(this)" dur="20s"/>
+ </rect>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test SMIL repeat timing **/
+
+/* Global Variables */
+const gTimeoutDur = 5000; // Time until we give up waiting for events in ms
+var gSvg = document.getElementById('svg');
+var gRect = document.getElementById('circle');
+var gTimeoutID;
+var gGotBegin = false;
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+function testBegin()
+{
+ gSvg.setCurrentTime(19.999);
+}
+
+function startWaiting(evt)
+{
+ is(evt.detail, 1, "Unexpected repeat event received: test broken");
+ if (gGotBegin)
+ return;
+
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+}
+
+function timeoutFail()
+{
+ ok(false, "Timed out waiting for begin event");
+ finish();
+}
+
+function expectedBegin()
+{
+ is(gGotBegin, false,
+ "Got begin event more than once for non-repeating animation");
+ gGotBegin = true;
+ clearTimeout(gTimeoutID);
+ // Wait a moment before finishing in case there are erroneous events waiting
+ // to be processed.
+ setTimeout(finish, 10);
+}
+
+function unexpectedBegin(elem)
+{
+ ok(false, "Got unexpected begin from animation with spec: " +
+ elem.getAttribute('begin'));
+}
+
+function finish()
+{
+ gSvg.pauseAnimations();
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", testBegin, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Tests for SMIL Reset Behavior </title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" from="20" to="100" begin="2s" dur="4s"
+ id="anim1" attributeType="XML"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Tests for SMIL Reset Behavior **/
+
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ var svg = document.getElementById("svg");
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var anim = document.getElementById("anim1");
+ is(anim.getStartTime(), 2, "Unexpected initial start time");
+
+ svg.setCurrentTime(1);
+ anim.beginElementAt(2);
+
+ // We now have two instance times: 2, 3
+
+ // Restart (and reset) animation at t=1
+ anim.beginElement();
+
+ // Instance times should now be 1, 2 (3 should have be reset)
+ is(anim.getStartTime(), 1,
+ "Unexpected start time after restart. Perhaps the added instance time "
+ + "was cleared");
+ svg.setCurrentTime(4);
+ // Instance times will now be 2 (1 will have be reset when we restarted)
+ is(anim.getStartTime(), 2, "Unexpected start time after seek");
+
+ // Create a two new instance times at t=4, 5
+ anim.beginElement();
+ anim.beginElementAt(1);
+ is(anim.getStartTime(), 4, "Unexpected start time after beginElement");
+
+ // Here is a white box test to make sure we don't discard instance times
+ // created by DOM calls when setting/unsetting the 'begin' spec
+ anim.removeAttribute('begin');
+ is(anim.getStartTime(), 4, "Unexpected start time after clearing begin spec");
+ svg.setCurrentTime(6);
+ is(anim.getStartTime(), 5,
+ "Second DOM instance time cleared when begin spec was removed");
+
+ // And likewise, when we set it again
+ anim.beginElementAt(1); // Instance times now t=5s, 7s
+ anim.setAttribute('begin', '1s'); // + t=1s
+ is(anim.getStartTime(), 5, "Unexpected start time after setting begin spec");
+ svg.setCurrentTime(8);
+ is(anim.getStartTime(), 7,
+ "Second DOM instance time cleared when begin spec was added");
+
+ // But check we do update state appropriately
+ anim.setAttribute('begin', '8s');
+ is(anim.getStartTime(), 8, "Interval not updated with updated begin spec");
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL Restart Behavior </title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <!-- These 3 circles only differ in their animation's "restart" value -->
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" from="20" to="100" begin="1s" dur="4s"
+ restart="always" id="always" attributeType="XML"/>
+ </circle>
+ <circle cx="20" cy="60" r="15" fill="blue">
+ <animate attributeName="cx" from="20" to="100" begin="1s" dur="4s"
+ restart="whenNotActive" id="whenNotActive" attributeType="XML"/>
+ </circle>
+ <circle cx="20" cy="100" r="15" fill="blue">
+ <animate attributeName="cx" from="20" to="100" begin="1s" dur="4s"
+ restart="never" id="never" attributeType="XML"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL Restart Behavior **/
+
+/* Global Variables */
+var svg = document.getElementById("svg");
+var always = document.getElementById("always");
+var whenNotActive = document.getElementById("whenNotActive");
+var never = document.getElementById("never");
+
+SimpleTest.waitForExplicitFinish();
+
+function tryRestart(elem, state, expected) {
+ var restartTime = svg.getCurrentTime();
+ elem.beginElement();
+ var restart = false;
+ try {
+ restart = (elem.getStartTime() === restartTime);
+ } catch (e) {
+ if (e.name != "InvalidStateError" ||
+ e.code != DOMException.INVALID_STATE_ERR)
+ throw e;
+ restart = false;
+ }
+ if (expected) {
+ var msg = elem.id + " can't restart in " + state + " state";
+ ok(restart, msg);
+ } else {
+ var msg = elem.id + " can restart in " + state + " state";
+ ok(!restart, msg);
+ }
+}
+
+function main() {
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ // At first everything should be starting at 1s
+ is(always.getStartTime(), 1);
+ is(whenNotActive.getStartTime(), 1);
+ is(never.getStartTime(), 1);
+
+ // Now try to restart everything early, should be allowed by all
+ tryRestart(always, "waiting", true);
+ tryRestart(whenNotActive, "waiting", true);
+ tryRestart(never, "waiting", true);
+
+ // Now skip to half-way
+ var newTime = always.getStartTime() + 0.5 * always.getSimpleDuration();
+ svg.setCurrentTime(newTime);
+
+ // Only 'always' should be able to be restarted
+ tryRestart(always, "active", true);
+ tryRestart(whenNotActive, "active", false);
+ tryRestart(never, "active", false);
+
+ // Now skip to the end
+ newTime = always.getStartTime() + always.getSimpleDuration() + 1;
+ svg.setCurrentTime(newTime);
+
+ // All animations have finished, so 'always' and 'whenNotActive' should be
+ // able to be restarted
+ tryRestart(always, "postactive", true);
+ tryRestart(whenNotActive, "postactive", true);
+ tryRestart(never, "postactive", false);
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for setCurrentTime Behavior </title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg"
+ onload="this.pauseAnimations()" />
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for basic setCurrentTime / getCurrentTime Behavior **/
+
+/* Global Variables & Constants */
+const PRECISION_LEVEL = 0.0000001; // Allow small level of floating-point error
+const gTimes = [0, 1.5, 0.2, 0.99, -400.5, 10000000, -1];
+const gWaitTime = 20;
+var gSvg = document.getElementById("svg");
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+function main() {
+ ok(gSvg.animationsPaused(), "should be paused by <svg> load handler");
+ is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ // Test that seeking takes effect immediately
+ for (var i = 0; i < gTimes.length; i++) {
+ gSvg.setCurrentTime(gTimes[i]);
+ // We adopt the SVGT1.2 behavior of clamping negative times to 0
+ assertFloatsEqual(gSvg.getCurrentTime(), Math.max(gTimes[i], 0.0));
+ }
+
+ // Test that seeking isn't messed up by timeouts
+ // (using tail recursion to set up the chain of timeout function calls)
+ var func = function() {
+ checkTimesAfterIndex(0);
+ }
+ setTimeout(func, gWaitTime);
+}
+
+/* This method seeks to the time at gTimes[index],
+ * and then sets up a timeout to...
+ * - verify that the seek worked
+ * - make a recursive call for the next index.
+ */
+function checkTimesAfterIndex(index) {
+ if (index == gTimes.length) {
+ // base case -- we're done!
+ SimpleTest.finish();
+ return;
+ }
+
+ gSvg.setCurrentTime(gTimes[index]);
+ var func = function() {
+ assertFloatsEqual(gSvg.getCurrentTime(), Math.max(gTimes[index], 0.0));
+ checkTimesAfterIndex(index + 1);
+ }
+ setTimeout(func, gWaitTime);
+}
+
+function assertFloatsEqual(aVal, aExpected) {
+ ok(Math.abs(aVal - aExpected) <= PRECISION_LEVEL,
+ "getCurrentTime returned " + aVal + " after seeking to " + aExpected)
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL sync behaviour </title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px">
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" attributeType="XML" from="20" to="100"
+ begin="indefinite" dur="4s" restart="always" id="anim1"/>
+ </circle>
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" attributeType="XML" from="0" to="50"
+ begin="0" dur="1s" additive="sum" fill="freeze" id="anim2"/>
+ </circle>
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" attributeType="XML" from="0" to="50"
+ begin="0" dur="10s" additive="sum" fill="freeze" id="anim3"/>
+ </circle>
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" attributeType="XML" from="0" to="50"
+ begin="0" dur="10s" additive="sum" fill="freeze" id="anim4"/>
+ </circle>
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" attributeType="XML" from="0" to="50"
+ begin="0" dur="40s" additive="sum" fill="freeze" id="anim5"/>
+ </circle>
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animate attributeName="cx" attributeType="XML" from="20" to="100"
+ begin="100s" dur="4s" restart="always" id="anim6"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL sync behavior **/
+
+/* Global Variables */
+var svg = document.getElementById("svg");
+
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ testBeginAt(document.getElementById("anim1"));
+ testChangeBaseVal(document.getElementById("anim2"));
+ testChangeWhilePaused(document.getElementById("anim3"));
+ testChangeAnimAttribute(document.getElementById("anim4"));
+ testChangeTimingAttribute(document.getElementById("anim5"));
+ testSetCurrentTime(document.getElementById("anim6"));
+ SimpleTest.finish();
+}
+
+function testBeginAt(anim) {
+ // This (hugely important) test checks that a call to beginElement updates to
+ // the new interval
+
+ // Check some pre-conditions
+ is(anim.getAttribute("restart"), "always");
+ ok(anim.getSimpleDuration() >= 4);
+
+ // First start the animation
+ svg.setCurrentTime(2);
+ anim.beginElement();
+
+ // Then restart it--twice
+ svg.setCurrentTime(4);
+ anim.beginElement();
+ anim.beginElementAt(-1);
+
+ // The first restart should win if the state machine has been successfully
+ // updated. If we get '3' back instead we haven't updated properly.
+ is(anim.getStartTime(), 4);
+}
+
+function testChangeBaseVal(anim) {
+ // Check that a change to the base value is updated even after animation is
+ // frozen
+
+ // preconditions -- element should have ended
+ try {
+ anim.getStartTime();
+ ok(false, "Element has not ended yet.");
+ } catch (e) { }
+
+ // check frozen value is applied
+ var target = anim.targetElement;
+ is(target.cx.animVal.value, 70);
+ is(target.cx.baseVal.value, 20);
+
+ // change base val and re-check
+ target.cx.baseVal.value = 30;
+ is(target.cx.animVal.value, 80);
+ is(target.cx.baseVal.value, 30);
+}
+
+function testChangeWhilePaused(anim) {
+ // Check that a change to the base value is updated even when the animation is
+ // paused
+
+ svg.pauseAnimations();
+ svg.setCurrentTime(anim.getSimpleDuration() / 2);
+
+ // check paused value is applied
+ var target = anim.targetElement;
+ is(target.cx.animVal.value, 45);
+ is(target.cx.baseVal.value, 20);
+
+ // change base val and re-check
+ target.cx.baseVal.value = 30;
+ is(target.cx.animVal.value, 55);
+ is(target.cx.baseVal.value, 30);
+}
+
+function testChangeAnimAttribute(anim) {
+ // Check that a change to an animation attribute causes an update even when
+ // the animation is frozen and paused
+
+ // Make sure animation is paused and frozen
+ svg.pauseAnimations();
+ svg.setCurrentTime(anim.getStartTime() + anim.getSimpleDuration() + 1);
+
+ // Check frozen value is applied
+ var target = anim.targetElement;
+ is(target.cx.animVal.value, 70);
+ is(target.cx.baseVal.value, 20);
+
+ // Make the animation no longer additive
+ anim.removeAttribute("additive");
+ is(target.cx.animVal.value, 50);
+ is(target.cx.baseVal.value, 20);
+}
+
+function testChangeTimingAttribute(anim) {
+ // Check that a change to a timing attribute causes an update even when
+ // the animation is paused
+
+ svg.pauseAnimations();
+ svg.setCurrentTime(anim.getSimpleDuration() / 2);
+
+ // Check part-way value is applied
+ var target = anim.targetElement;
+ is(target.cx.animVal.value, 45);
+ is(target.cx.baseVal.value, 20);
+
+ // Make the animation no longer additive
+ anim.setAttribute("dur", String(anim.getSimpleDuration() / 2) + "s");
+ is(target.cx.animVal.value, 70);
+ is(target.cx.baseVal.value, 20);
+
+ // Remove fill
+ anim.removeAttribute("fill");
+ is(target.cx.animVal.value, 20);
+ is(target.cx.baseVal.value, 20);
+}
+
+function testSetCurrentTime(anim) {
+ // This test checks that a call to setCurrentTime flushes restarts
+ //
+ // Actually, this same scenario arises in test_smilRestart.xhtml but we
+ // isolate this particular situation here for easier diagnosis if this ever
+ // fails.
+ //
+ // At first we have:
+ // currentTime begin="100s"
+ // v v
+ // Doc time: 0---\/\/\/-------99----------100-------
+ //
+ svg.setCurrentTime(99);
+ is(anim.getStartTime(), 100);
+
+ // Then we restart giving us:
+ //
+ // beginElement begin="100s"
+ // v v
+ // Doc time: 0---\/\/\/-------99----------100-------
+ //
+ // So our current interval is
+ //
+ // begin="100s"
+ // v
+ // +---------------|
+ // Doc time: 0---\/\/\/-------99-100-101-102-103-----
+ //
+ anim.beginElement();
+ is(anim.getStartTime(), svg.getCurrentTime());
+
+ // Then we skip to half-way, i.e.
+ //
+ // currentTime
+ // v
+ // begin="100s"
+ // v
+ // +---------------|
+ // Doc time: 0---\/\/\/-------99-100-101-102-103-----
+ //
+ // At this point we should flush our restarts and early end the first interval
+ // and start the second interval, giving us
+ //
+ // So our timegraph looks like:
+ //
+ // currentTime
+ // v
+ // +---------------|
+ // +---|
+ // Doc time: 0---\/\/\/-------99-100-101-102-103-104-
+ //
+ var newTime = anim.getStartTime() + 0.5 * anim.getSimpleDuration();
+ svg.setCurrentTime(newTime);
+
+ // Finally we call beginElement again giving us
+ //
+ // currentTime
+ // v
+ // +---------------|
+ // +---|
+ // +---|
+ // Doc time: 0---\/\/\/-------99-100-101-102-103-104-105-
+ //
+ // If, however, setCurrentTime failed to flush restarts out starting point
+ // we do come to update the timegraph would be:
+ //
+ // beginElementAt
+ // v
+ // begin="100s"
+ // v
+ // +---------------|
+ // Doc time: 0---\/\/\/-------99-100-101-102-103-----
+ //
+ // And as soon as we encountered the begin="100s" spec we'd do a restart
+ // according to the SMIL algorithms and a restart involves a reset which
+ // clears the instance times created by DOM calls and so we'd end up with
+ // just:
+ //
+ // currentTime
+ // v
+ // +---------------|
+ // +---|
+ // Doc time: 0---\/\/\/-------99-100-101-102-103-104-
+ //
+ // Which is probably not what the author intended.
+ //
+ anim.beginElement();
+ is(anim.getStartTime(), svg.getCurrentTime());
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL sync behaviour for transform types</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px">
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animateTransform attributeName="transform" type="rotate"
+ from="90" to="180" begin="0s" dur="2s" fill="freeze"
+ additive="sum" id="anim1"/>
+ </circle>
+ <circle cx="20" cy="20" r="15" fill="blue">
+ <animateTransform attributeName="transform" type="scale"
+ from="1" to="2" begin="2s" dur="2s" id="anim2"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL sync behavior for transform types **/
+
+/* Global Variables */
+var svg = document.getElementById("svg");
+
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ testChangeBaseVal(document.getElementById("anim1"));
+ SimpleTest.finish();
+}
+
+function testChangeBaseVal(anim) {
+ // Check that a change to the base value is updated even after animation is
+ // frozen
+
+ var target = anim.targetElement;
+
+ var baseList = target.transform.baseVal;
+ var animList = target.transform.animVal;
+
+ // make sure element has ended
+ svg.setCurrentTime(anim.getSimpleDuration());
+
+ // check frozen value is applied
+ is(baseList.numberOfItems, 0);
+ is(animList.numberOfItems, 1);
+
+ // change base val and re-check
+ var newTransform = svg.createSVGTransform();
+ newTransform.setScale(1,2);
+ baseList.appendItem(newTransform);
+ is(baseList.numberOfItems, 1);
+ is(animList.numberOfItems, 2);
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for syncbase targetting</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="-20" cy="20" r="15" fill="blue" id="circle">
+ <set attributeName="cx" to="0" begin="2s" dur="1s" id="a"/>
+ <set attributeName="cx" to="0" begin="2s" dur="1s" xml:id="b"/>
+ <set attributeName="cx" to="0" begin="2s" dur="1s" id="あ"/>
+ <set attributeName="cx" to="0" begin="2s" dur="1s" id="a.b"/>
+ <set attributeName="cx" to="0" begin="2s" dur="1s" id="a-b"/>
+ <set attributeName="cx" to="0" begin="2s" dur="1s" id="a:b"/>
+ <set attributeName="cx" to="0" begin="2s" dur="1s" id="-a"/>
+ <set attributeName="cx" to="0" begin="2s" dur="1s" id="0"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for syncbase targetting behavior **/
+
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ var svg = getElement("svg");
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ testSpecs();
+ testChangeId();
+ testRemoveTimebase();
+
+ SimpleTest.finish();
+}
+
+function testSpecs() {
+ var anim = createAnim();
+
+ // Sanity check--initial state
+ ok(noStart(anim), "Unexpected initial value for indefinite start time.");
+
+ var specs = [ [ 'a.begin', 2 ],
+ [ 'b.begin', 'todo' ], // xml:id support, bug 275196
+ [ 'あ.begin', 2 ], // unicode id
+ [ ' a.begin ', 2 ], // whitespace
+ [ 'a\\.b.begin', 2 ], // escaping
+ [ 'a\\-b.begin', 2 ], // escaping
+ [ 'a:b.begin', 2 ],
+ // Invalid
+ [ '-a.begin', 'notok' ], // invalid XML ID
+ [ '\\-a.begin', 'notok' ], // invalid XML ID
+ [ '0.begin', 'notok' ], // invalid XML ID
+ [ '\xB7.begin', 'notok' ], // invalid XML ID
+ [ '\x7B.begin', 'notok' ], // invalid XML ID
+ [ '.begin', 'notok' ],
+ [ ' .end ', 'notok' ],
+ [ 'a.begin-5a', 'notok' ],
+ // Offsets
+ [ ' a.begin + 1min', 2 + 60 ],
+ [ ' a.begin-0.5s', 1.5 ],
+ ];
+ for (var i = 0; i < specs.length; i++) {
+ var spec = specs[i][0];
+ var expected = specs[i][1];
+ anim.setAttribute('begin', spec);
+ try {
+ if (typeof(expected) == 'number') {
+ is(anim.getStartTime(), expected,
+ "Unexpected start time with spec: " + spec);
+ } else if (expected == 'todo') {
+ todo_is(anim.getStartTime(), 2,"Unexpected success with spec: " + spec);
+ } else {
+ anim.getStartTime();
+ ok(false, "Unexpected success with spec: " + spec);
+ }
+ } catch(e) {
+ if (e.name == "InvalidStateError" &&
+ e.code == DOMException.INVALID_STATE_ERR) {
+ if (typeof(expected) == 'number')
+ ok(false, "Failed with spec: " + spec);
+ else if (expected == 'todo')
+ todo(false, "Yet to implement: " + spec);
+ else
+ ok(true);
+ } else {
+ ok(false, "Unexpected exception: " + e + "(with spec: " + spec + ")");
+ }
+ }
+ }
+
+ anim.parentNode.removeChild(anim);
+}
+
+function testChangeId() {
+ var anim = createAnim();
+
+ anim.setAttribute('begin', 'a.begin');
+ is(anim.getStartTime(), 2, "Unexpected start time.");
+
+ var a = getElement('a');
+ a.setAttribute('id', 'a1');
+ ok(noStart(anim), "Unexpected return value after changing target ID.");
+
+ a.setAttribute('id', 'a');
+ is(anim.getStartTime(), 2,
+ "Unexpected start time after resetting target ID.");
+
+ anim.parentNode.removeChild(anim);
+}
+
+function testRemoveTimebase() {
+ var anim = createAnim();
+ anim.setAttribute('begin', 'a.begin');
+ ok(!noStart(anim), "Unexpected start time before removing timebase.");
+
+ var circle = getElement('circle');
+ var a = getElement('a');
+ // Sanity check
+ is(a, circle.firstElementChild, "Unexpected document structure");
+
+ // Remove timebase
+ a.parentNode.removeChild(a);
+ ok(noStart(anim), "Unexpected start time after removing timebase.");
+
+ // Reinsert timebase
+ circle.insertBefore(a, circle.firstElementChild);
+ ok(!noStart(anim), "Unexpected start time after re-inserting timebase.");
+
+ // Remove dependent element
+ anim.parentNode.removeChild(anim);
+ ok(noStart(anim), "Unexpected start time after removing dependent.");
+
+ // Create a new dependent
+ var anim2 = createAnim();
+ anim2.setAttribute('begin', 'a.begin');
+ is(anim2.getStartTime(), 2,
+ "Unexpected start time after adding new dependent.");
+}
+
+function createAnim() {
+ const svgns="http://www.w3.org/2000/svg";
+ var anim = document.createElementNS(svgns,'animate');
+ anim.setAttribute('attributeName','cx');
+ anim.setAttribute('from','0');
+ anim.setAttribute('to','100');
+ anim.setAttribute('begin','indefinite');
+ anim.setAttribute('dur','1s');
+ return getElement('circle').appendChild(anim);
+}
+
+function noStart(elem) {
+ var exceptionCaught = false;
+
+ try {
+ elem.getStartTime();
+ } catch(e) {
+ exceptionCaught = true;
+ is (e.name, "InvalidStateError",
+ "Unexpected exception from getStartTime.");
+ is (e.code, DOMException.INVALID_STATE_ERR,
+ "Unexpected exception code from getStartTime.");
+ }
+
+ return exceptionCaught;
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL Animation Behavior with textZoom</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+ <svg xmlns="http://www.w3.org/2000/svg" width="300px" height="200px"
+ onload="this.pauseAnimations()">
+ <text y="100px" x="0px" style="font-size: 5px">
+ abc
+ <animate attributeName="font-size" attributeType="CSS" fill="freeze"
+ from="20px" to="40px" begin="1s" dur="1s"/>
+ </text>
+ <rect y="100px" x="50px" style="stroke-width: 5px">
+ <animate attributeName="stroke-width" attributeType="CSS" fill="freeze"
+ from="20px" to="40px" begin="1s" dur="1s"/>
+ </rect>
+ </svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+// Helper function
+function verifyStyle(aNode, aPropertyName, aExpectedVal)
+{
+ var computedVal = SMILUtil.getComputedStyleSimple(aNode, aPropertyName);
+ is(computedVal, aExpectedVal, "computed value of " + aPropertyName);
+}
+
+function main()
+{
+ // Start out pause
+ var svg = SMILUtil.getSVGRoot();
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ // Set text zoom to 2x
+ var origTextZoom = SpecialPowers.getTextZoom(window);
+ SpecialPowers.setTextZoom(window, 2);
+
+ try {
+ // Verify computed style values at various points during animation.
+ // * Correct behavior is for the computed values of 'font-size' to be
+ // the same as their corresponding specified values, since text zoom
+ // should not affect SVG text elements.
+ // * I also include tests for an identical animation of the "stroke-width"
+ // property, which should _not_ be affected by textZoom.
+ var text = document.getElementsByTagName("text")[0];
+ var rect = document.getElementsByTagName("rect")[0];
+
+ verifyStyle(text, "font-size", "5px");
+ verifyStyle(rect, "stroke-width", "5px");
+ svg.setCurrentTime(1);
+ verifyStyle(text, "font-size", "20px");
+ verifyStyle(rect, "stroke-width", "20");
+ svg.setCurrentTime(1.5);
+ verifyStyle(text, "font-size", "30px");
+ verifyStyle(rect, "stroke-width", "30");
+ svg.setCurrentTime(2);
+ verifyStyle(text, "font-size", "40px");
+ verifyStyle(rect, "stroke-width", "40");
+ svg.setCurrentTime(3);
+ verifyStyle(text, "font-size", "40px");
+ verifyStyle(rect, "stroke-width", "40");
+ } catch (e) {
+ // If anything goes wrong, make sure we restore textZoom before bubbling
+ // the exception upwards, so that we don't mess up subsequent tests.
+ SpecialPowers.setTextZoom(window, origTextZoom);
+
+ throw e;
+ }
+
+ // We're done! Restore original text-zoom before finishing
+ SpecialPowers.setTextZoom(window, origTextZoom);
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=572270
+-->
+<head>
+ <title>Test TimeEvents dispatching</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=572270">Mozilla Bug
+ 572270</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="100px" height="100px">
+ <g font-size="10px">
+ <circle cx="0" cy="0" r="15" fill="blue" id="circle"
+ onbegin="parentHandler(evt)" onrepeat="parentHandler(evt)"
+ onend="parentHandler(evt)">
+ <animate attributeName="cy" from="0" to="100" dur="60s" begin="2s"
+ id="anim" repeatCount="2"
+ onbegin="handleOnBegin(evt)" onrepeat="handleOnRepeat(evt)"
+ onend="handleOnEnd(evt)"/>
+ </circle>
+ </g>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test SMIL TimeEvents dispatching **/
+
+/* Global Variables */
+const gTimeoutDur = 60000; // Time until we give up waiting for events in ms
+var gSvg = document.getElementById("svg");
+var gAnim = document.getElementById('anim');
+var gCircle = document.getElementById('circle');
+var gExpectedEvents = new Array();
+var gTimeoutID;
+var gTestStages =
+ [ testPlaybackBegin,
+ testPlaybackRepeat,
+ testPlaybackEnd,
+ testForwardsSeekToMid,
+ testForwardsSeekToNextInterval,
+ testForwardsSeekPastEnd,
+ testBackwardsSeekToMid,
+ testBackwardsSeekToStart,
+ testCreateEvent,
+ testRegistration
+ ];
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+function continueTest()
+{
+ if (gTestStages.length == 0) {
+ SimpleTest.finish();
+ return;
+ }
+ gTestStages.shift()();
+}
+
+function testPlaybackBegin()
+{
+ // Test events are dispatched through normal playback
+ gSvg.pauseAnimations();
+ gSvg.setCurrentTime(1.99);
+ gExpectedEvents.push("beginEvent", "beginEvent"); // Two registered handlers
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gSvg.unpauseAnimations();
+}
+
+function testPlaybackRepeat()
+{
+ gSvg.pauseAnimations();
+ gSvg.setCurrentTime(61.99);
+ gExpectedEvents.push(["repeatEvent", 1], ["repeatEvent", 1]);
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gSvg.unpauseAnimations();
+}
+
+function testPlaybackEnd()
+{
+ gSvg.pauseAnimations();
+ gSvg.setCurrentTime(121.99);
+ gExpectedEvents.push("endEvent", "endEvent");
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gSvg.unpauseAnimations();
+}
+
+function testForwardsSeekToMid()
+{
+ gSvg.pauseAnimations();
+ // Set animation parameters to something that repeats a lot
+ gSvg.setCurrentTime(0);
+ gAnim.setAttribute('begin', '2s; 102s');
+ gAnim.setAttribute('dur', '15s');
+ gAnim.setAttribute('repeatCount', '6');
+ gSvg.setCurrentTime(46.99);
+ gExpectedEvents.push("beginEvent", "beginEvent",
+ ["repeatEvent", 3], ["repeatEvent", 3]);
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gSvg.unpauseAnimations();
+}
+
+function testForwardsSeekToNextInterval()
+{
+ // Skip to next interval -- we shouldn't get any additional begin or end
+ // events in between
+ gSvg.pauseAnimations();
+ gSvg.setCurrentTime(131.99);
+ gExpectedEvents.push(["repeatEvent", 2], ["repeatEvent", 2]);
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gSvg.unpauseAnimations();
+}
+
+function testForwardsSeekPastEnd()
+{
+ gSvg.pauseAnimations();
+ gSvg.setCurrentTime(200);
+ gExpectedEvents.push("endEvent", "endEvent");
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gSvg.unpauseAnimations();
+}
+
+function testBackwardsSeekToMid()
+{
+ gSvg.pauseAnimations();
+ gSvg.setCurrentTime(31.99);
+ gExpectedEvents.push("beginEvent", "beginEvent",
+ ["repeatEvent", 2], ["repeatEvent", 2]);
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gSvg.unpauseAnimations();
+}
+
+function testBackwardsSeekToStart()
+{
+ gSvg.pauseAnimations();
+ gExpectedEvents.push("endEvent", "endEvent");
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gSvg.setCurrentTime(0);
+}
+
+function testCreateEvent()
+{
+ var evt;
+ try {
+ evt = document.createEvent("TimeEvents");
+ } catch (e) {
+ ok(false, "Failed to create TimeEvent via script: " + e);
+ return;
+ }
+ evt.initTimeEvent("repeatEvent", null, 3);
+ is(evt.type, "repeatEvent", "Unexpected type for user-generated event");
+ is(evt.detail, 3, "Unexpected detail for user-generated event");
+ is(evt.target, null, "Unexpected event target");
+ is(evt.currentTarget, null, "Unexpected event current target");
+ is(evt.eventPhase, evt.NONE);
+ is(evt.bubbles, false, "Event should not bubble");
+ is(evt.cancelable, false, "Event should not be cancelable");
+ is(evt.view, null, "Event view should be null");
+
+ // Prior to dispatch we should be able to change the event type
+ evt.initTimeEvent("beginEvent", document.defaultView, 0);
+ is(evt.type, "beginEvent", "Failed to update event type before dispatch");
+ is(evt.detail, 0, "Failed to update event detail before dispatch");
+ is(evt.view, document.defaultView, "Event view should be set");
+
+ // But not directly as it's readonly
+ try {
+ evt.type = "endEvent";
+ } catch(e) { }
+ is(evt.type, "beginEvent", "Event type should be readonly");
+
+ // Likewise the detail field should be readonly
+ try {
+ evt.detail = "8";
+ } catch(e) { }
+ is(evt.detail, 0, "Event detail should be readonly");
+
+ // Dispatch
+ gExpectedEvents.push("beginEvent", "beginEvent");
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gAnim.dispatchEvent(evt);
+}
+
+function testRegistration()
+{
+ gSvg.pauseAnimations();
+ // Reset animation to something simple
+ gSvg.setCurrentTime(0);
+ gAnim.setAttribute('begin', '2s');
+ gAnim.setAttribute('dur', '50s');
+
+ // Remove attribute handler
+ gAnim.removeAttribute('onbegin');
+
+ // Add bogus handlers
+ gAnim.setAttribute('onbeginElement', 'handleOnBegin(evt)');
+ gAnim.addEventListener("begin", handleOnBegin, false);
+ gAnim.addEventListener("onbegin", handleOnBegin, false);
+
+ // We should now have just one legitimate listener: the one registered to
+ // handle 'beginElement'
+ gSvg.setCurrentTime(1.99);
+ gExpectedEvents.push("beginEvent");
+ gTimeoutID = setTimeout(timeoutFail, gTimeoutDur);
+ gSvg.unpauseAnimations();
+}
+
+function handleOnBegin(evt)
+{
+ is(evt.type, "beginEvent", "Expected begin event but got " + evt.type);
+ checkExpectedEvent(evt);
+}
+
+function handleOnRepeat(evt)
+{
+ is(evt.type, "repeatEvent", "Expected repeat event but got " + evt.type);
+ checkExpectedEvent(evt);
+}
+
+function handleOnEnd(evt)
+{
+ is(evt.type, "endEvent", "Expected end event but got " + evt.type);
+ checkExpectedEvent(evt);
+}
+
+function sanityCheckEvent(evt)
+{
+ is(evt.target, gAnim, "Unexpected event target");
+ is(evt.currentTarget, gAnim, "Unexpected event current target");
+ is(evt.eventPhase, evt.AT_TARGET);
+ is(evt.bubbles, false, "Event should not bubble");
+ is(evt.cancelable, false, "Event should not be cancelable");
+ if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) {
+ var now = window.performance.now();
+ ok(evt.timeStamp > 0 && evt.timeStamp < now,
+ "Event timeStamp (" + evt.timeStamp + ") should be > 0 but " +
+ "before the current time (" + now + ")");
+ } else {
+ is(evt.timeStamp, 0, "Event timeStamp should be 0");
+ }
+ ok(evt.view !== null, "Event view not set");
+}
+
+function checkExpectedEvent(evt)
+{
+ sanityCheckEvent(evt);
+ ok(gExpectedEvents.length > 0, "Unexpected event: " + evt.type);
+ if (gExpectedEvents.length == 0) return;
+
+ var expected = gExpectedEvents.shift();
+ if (typeof expected == 'string') {
+ is(evt.type, expected, "Unexpected event type");
+ is(evt.detail, 0, "Unexpected event detail (repeat iteration)");
+ } else {
+ is(evt.type, expected[0], "Unexpected event type");
+ is(evt.detail, expected[1], "Unexpected event detail (repeat iteration)");
+ }
+ if (gExpectedEvents.length == 0) {
+ clearTimeout(gTimeoutID);
+ continueTest();
+ }
+}
+
+function timeoutFail()
+{
+ ok(false, "Timed out waiting for events: " + gExpectedEvents.join(', '));
+ SimpleTest.finish(); // No point continuing
+}
+
+function parentHandler(evt)
+{
+ ok(false, "Handler on parent got called but event shouldn't bubble.");
+}
+
+window.addEventListener("load", continueTest, false);
+
+// Register event handlers *in addition* to the handlers already added via the
+// "onbegin", "onend", "onrepeat" attributes on the <animate> and <circle>
+// elements. This is to test that both types of registration work.
+gAnim.addEventListener("beginEvent", handleOnBegin, false);
+gAnim.addEventListener("repeatEvent", handleOnRepeat, false);
+gAnim.addEventListener("endEvent", handleOnEnd, false);
+gCircle.addEventListener("beginEvent", parentHandler, false);
+
+var expectedEvents =
+ ["begin", "beginEvent", "repeat", "repeatEvent", "end", "endEvent", "SVGZoom", "zoom"];
+
+for (var i = 0; i < expectedEvents.length; ++i) {
+ is((new Event(expectedEvents[i])).type, expectedEvents[i], "Unexpected event type!");
+}
+
+var timeEvents = ["begin", "repeat", "end"];
+var expectedEvents = ["begin", "beginEvent", "repeat", "repeatEvent", "end", "endEvent"];
+var d = document.createElement("div");
+for (var i = 0; i < timeEvents.length; ++i) {
+ d.addEventListener(timeEvents[i], function(e) {
+ is(e.type, expectedEvents[0], "Got the expected event type.");
+ expectedEvents.shift();
+ });
+
+ // Without "Event" suffix.
+ var e = document.createEvent("timeevent");
+ e.initEvent(timeEvents[i], true, true);
+ d.dispatchEvent(e);
+
+ // With "Event" suffix.
+ e = document.createEvent("timeevent");
+ e.initEvent(timeEvents[i] + "Event", true, true);
+ d.dispatchEvent(e);
+}
+is(expectedEvents.length, 0, "Got all the expected events.");
+
+expectedEvents = ["zoom", "SVGZoom"];
+d.addEventListener("zoom", function(e) {
+ is(e.type, expectedEvents[0]);
+ expectedEvents.shift();
+});
+
+var zoomEvent = document.createEvent("svgzoomevent");
+zoomEvent.initEvent("zoom", true, true);
+d.dispatchEvent(zoomEvent);
+zoomEvent = document.createEvent("svgzoomevent");
+zoomEvent.initEvent("SVGZoom", true, true);
+d.dispatchEvent(zoomEvent);
+is(expectedEvents.length, 0, "Got all the expected events.");
+
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL timing</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL timing **/
+
+/* Global Variables */
+const svgns = "http://www.w3.org/2000/svg";
+var gSvg = document.getElementById("svg");
+var gCircle = document.getElementById('circle');
+
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ ok(gSvg.animationsPaused(), "should be paused by <svg> load handler");
+ is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var testCases = Array();
+
+ const secPerMin = 60;
+ const secPerHour = secPerMin * 60;
+
+ // In the following tests that compare start times, getStartTime will round
+ // the start time to three decimal places since we expect our implementation
+ // to be millisecond accurate.
+
+ // Offset syntax
+ // -- Basic tests, sign and whitespace
+ testCases.push(StartTimeTest('3s', 3));
+ testCases.push(StartTimeTest('0s', 0));
+ testCases.push(StartTimeTest('+2s', 2));
+ testCases.push(StartTimeTest('-1s\t\r', -1));
+ testCases.push(StartTimeTest('- 1s', -1));
+ testCases.push(StartTimeTest(' -1s', -1));
+ testCases.push(StartTimeTest(' - 1s', -1));
+ testCases.push(StartTimeTest(' \t\n\r-1s', -1));
+ testCases.push(StartTimeTest('+\n5s', 5));
+ testCases.push(StartTimeTest('-\n5s', -5));
+ testCases.push(StartTimeTest('\t 5s', 5));
+ // -- These tests are from SMILANIM 3.6.7
+ testCases.push(StartTimeTest('02:30:03', 2*secPerHour + 30*secPerMin + 3));
+ testCases.push(StartTimeTest('50:00:10.25', 50*secPerHour + 10.25));
+ testCases.push(StartTimeTest('02:33', 2*secPerMin + 33));
+ testCases.push(StartTimeTest('00:10.5', 10.5));
+ testCases.push(StartTimeTest('3.2h', 3.2*secPerHour));
+ testCases.push(StartTimeTest('45min', 45*secPerMin));
+ testCases.push(StartTimeTest('30s', 30));
+ testCases.push(StartTimeTest('5ms', 0.005));
+ testCases.push(StartTimeTest('12.467', 12.467));
+ testCases.push(StartTimeTest('00.5s', 0.5));
+ testCases.push(StartTimeTest('00:00.005', 0.005));
+ // -- Additional tests
+ testCases.push(StartTimeTest('61:59:59', 61*secPerHour + 59*secPerMin + 59));
+ testCases.push(StartTimeTest('02:59.999999999999999999999', 3*secPerMin));
+ testCases.push(StartTimeTest('1234:23:45',
+ 1234*secPerHour + 23*secPerMin + 45));
+ testCases.push(StartTimeTest('61min', 61*secPerMin));
+ testCases.push(StartTimeTest('0:30:03', 30*secPerMin + 3));
+ // -- Fractional precision
+ testCases.push(StartTimeTest('25.4567', 25.457));
+ testCases.push(StartTimeTest('0.123456789', 0.123));
+ testCases.push(StartTimeTest('0.00000000000000000000001', 0));
+ testCases.push(StartTimeTest('-0.00000000000000000000001', 0));
+ testCases.push(StartTimeTest('0.0009', 0.001));
+ testCases.push(StartTimeTest('0.99999999999999999999999999999999999999', 1));
+ testCases.push(StartTimeTest('23.4567ms', 0.023));
+ testCases.push(StartTimeTest('23.7ms', 0.024));
+ // -- Test errors
+ testCases.push(StartTimeTest(' + +3s', 'none'));
+ testCases.push(StartTimeTest(' +-3s', 'none'));
+ testCases.push(StartTimeTest('1:12:12:12', 'none'));
+ testCases.push(StartTimeTest('4:50:60', 'none'));
+ testCases.push(StartTimeTest('4:60:0', 'none'));
+ testCases.push(StartTimeTest('4:60', 'none'));
+ testCases.push(StartTimeTest('4:-1:00', 'none'));
+ testCases.push(StartTimeTest('4 5m', 'none'));
+ testCases.push(StartTimeTest('4 5ms', 'none'));
+ testCases.push(StartTimeTest('02:3:03', 'none'));
+ testCases.push(StartTimeTest('45.7 s', 'none'));
+ testCases.push(StartTimeTest(' 3 h ', 'none'));
+ testCases.push(StartTimeTest('2:33 ', 'none'));
+ testCases.push(StartTimeTest('02:33 2', 'none'));
+ testCases.push(StartTimeTest('\u000B 02:33', 'none'));
+ testCases.push(StartTimeTest('h', 'none'));
+ testCases.push(StartTimeTest('23.s', 'none'));
+ testCases.push(StartTimeTest('23.', 'none'));
+ testCases.push(StartTimeTest('23.54.2s', 'none'));
+ testCases.push(StartTimeTest('23sec', 'none'));
+ testCases.push(StartTimeTest('five', 'none'));
+ testCases.push(StartTimeTest('', 'none'));
+ testCases.push(StartTimeTest('02:33s', 'none'));
+ testCases.push(StartTimeTest('02:33 s', 'none'));
+ testCases.push(StartTimeTest('2.54e6', 'none'));
+ testCases.push(StartTimeTest('02.5:33', 'none'));
+ testCases.push(StartTimeTest('2:-45:33', 'none'));
+ testCases.push(StartTimeTest('2:4.5:33', 'none'));
+ testCases.push(StartTimeTest('45m', 'none'));
+ testCases.push(StartTimeTest(':20:30', 'none'));
+ testCases.push(StartTimeTest('1.5:30', 'none'));
+ testCases.push(StartTimeTest('15:-30', 'none'));
+ testCases.push(StartTimeTest('::30', 'none'));
+ testCases.push(StartTimeTest('15:30s', 'none'));
+ testCases.push(StartTimeTest('2:1.:30', 'none'));
+ testCases.push(StartTimeTest('2:.1:30', 'none'));
+ testCases.push(StartTimeTest('2.0:15:30', 'none'));
+ testCases.push(StartTimeTest('2.:15:30', 'none'));
+ testCases.push(StartTimeTest('.2:15:30', 'none'));
+ testCases.push(StartTimeTest('70:15', 'none'));
+ testCases.push(StartTimeTest('media', 'none'));
+ testCases.push(StartTimeTest('5mi', 'none'));
+ testCases.push(StartTimeTest('5hours', 'none'));
+ testCases.push(StartTimeTest('h05:30', 'none'));
+ testCases.push(StartTimeTest('05:40\x9A', 'none'));
+ testCases.push(StartTimeTest('05:40\u30D5', 'none'));
+ testCases.push(StartTimeTest('05:40β', 'none'));
+
+ // List syntax
+ testCases.push(StartTimeTest('3', 3));
+ testCases.push(StartTimeTest('3;', 3));
+ testCases.push(StartTimeTest('3; ', 3));
+ testCases.push(StartTimeTest('3 ; ', 3));
+ testCases.push(StartTimeTest('3;;', 'none'));
+ testCases.push(StartTimeTest('3;; ', 'none'));
+ testCases.push(StartTimeTest(';3', 'none'));
+ testCases.push(StartTimeTest(' ;3', 'none'));
+ testCases.push(StartTimeTest('3;4', 3));
+ testCases.push(StartTimeTest(' 3 ; 4 ', 3));
+
+ // List syntax on end times
+ testCases.push({
+ 'attr' : { 'begin': '0s',
+ 'end': '1s; 2s' },
+ 'times': [ [ 0, 0 ],
+ [ 1, -100 ] ]
+ });
+ testCases.push({
+ 'attr' : { 'begin': '0s',
+ 'end': '1s; 2s; ' },
+ 'times': [ [ 0, 0 ],
+ [ 1, -100 ] ]
+ });
+ testCases.push({
+ 'attr' : { 'begin': '0s',
+ 'end': '3s; 2s' },
+ 'times': [ [ 0, 0 ],
+ [ 1, 10 ],
+ [ 2, -100 ] ]
+ });
+
+ // Simple case
+ testCases.push({
+ 'attr' : { 'begin': '3s' },
+ 'times': [ [ 0, -100 ],
+ [ 4, 10 ] ]
+ });
+
+ // Multiple begins
+ testCases.push({
+ 'attr' : { 'begin': '2s; 6s',
+ 'dur': '2s' },
+ 'times': [ [ 0, -100 ],
+ [ 3, 50 ],
+ [ 4, -100 ],
+ [ 7, 50 ],
+ [ 8, -100 ] ]
+ });
+
+ // Negative begins
+ testCases.push({
+ 'attr' : { 'begin': '-3s; 1s ; 4s',
+ 'dur': '2s ',
+ 'fill': 'freeze' },
+ 'times': [ [ 0, -100 ],
+ [ 0.5, -100 ],
+ [ 1, 0 ],
+ [ 2, 50 ],
+ [ 3, 100 ],
+ [ 5, 50 ] ]
+ });
+
+ // Sorting
+ testCases.push({
+ 'attr' : { 'begin': '-3s; 110s; 1s; 4s; -5s; -10s',
+ 'end': '111s; -5s; -15s; 6s; -5s; 1.2s',
+ 'dur': '2s ',
+ 'fill': 'freeze' },
+ 'times': [ [ 0, -100 ],
+ [ 1, 0 ],
+ [ 2, 10 ],
+ [ 4, 0 ],
+ [ 5, 50 ],
+ [ 109, 100 ],
+ [ 110, 0 ],
+ [ 112, 50 ] ]
+ });
+
+ for (var i = 0; i < testCases.length; i++) {
+ gSvg.setCurrentTime(0);
+ var test = testCases[i];
+
+ // Generate string version of params for output messages
+ var params = "";
+ for (var name in test.attr) {
+ params += name + '="' + test.attr[name] + '" ';
+ }
+ params = params.trim();
+
+ // Create animation elements
+ var anim = createAnim(test.attr);
+
+ // Run samples
+ if ('times' in test) {
+ for (var j = 0; j < test.times.length; j++) {
+ var curSample = test.times[j];
+ checkSample(curSample[0], curSample[1], params);
+ }
+ }
+
+ // Check start time
+ if ('startTime' in test) {
+ is(getStartTime(anim), test.startTime,
+ "Got unexpected start time for " + params);
+ }
+
+ anim.parentNode.removeChild(anim);
+ }
+
+ SimpleTest.finish();
+}
+
+function createAnim(attr) {
+ var anim = document.createElementNS(svgns,'animate');
+ anim.setAttribute('attributeName','cx');
+ anim.setAttribute('from','0');
+ anim.setAttribute('to','100');
+ anim.setAttribute('dur','10s');
+ anim.setAttribute('begin','indefinite');
+ for (name in attr) {
+ anim.setAttribute(name, attr[name]);
+ }
+ return gCircle.appendChild(anim);
+}
+
+function checkSample(time, expectedValue, params) {
+ gSvg.setCurrentTime(time);
+ var msg = "Unexpected sample value for " + params +
+ " at t=" + time + ": ";
+ is(gCircle.cx.animVal.value, expectedValue);
+}
+
+function getStartTime(anim) {
+ var startTime;
+ try {
+ startTime = anim.getStartTime();
+ // We round start times to 3 decimal places to make comparisons simpler
+ startTime = parseFloat(startTime.toFixed(3));
+ } catch(e) {
+ if (e.name == "InvalidStateError" &&
+ e.code == DOMException.INVALID_STATE_ERR) {
+ startTime = 'none';
+ } else {
+ ok(false, "Unexpected exception: " + e);
+ }
+ }
+ return startTime;
+}
+
+function StartTimeTest(beginSpec, expectedStartTime) {
+ return { 'attr' : { 'begin': beginSpec },
+ 'startTime': expectedStartTime };
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL timing with zero-duration intervals</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL timing with zero-duration intervals **/
+
+/* Global Variables */
+const svgns="http://www.w3.org/2000/svg";
+var svg = document.getElementById("svg");
+var circle = document.getElementById('circle');
+
+SimpleTest.waitForExplicitFinish();
+
+function createAnim() {
+ var anim = document.createElementNS(svgns,'animate');
+ anim.setAttribute('attributeName','cx');
+ anim.setAttribute('from','0');
+ anim.setAttribute('to','100');
+ anim.setAttribute('dur','10s');
+ anim.setAttribute('begin','indefinite');
+ return circle.appendChild(anim);
+}
+
+function removeAnim(anim) {
+ anim.parentNode.removeChild(anim);
+}
+
+function main() {
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var tests =
+ [ testZeroDurationIntervalsA,
+ testZeroDurationIntervalsB,
+ testZeroDurationIntervalsC,
+ testZeroDurationIntervalsD,
+ testZeroDurationIntervalsE,
+ testZeroDurationIntervalsF,
+ testZeroDurationIntervalsG,
+ testZeroDurationIntervalsH,
+ testZeroDurationIntervalsI,
+ testZeroDurationIntervalsJ,
+ testZeroDurationIntervalsK,
+ testZeroDurationIntervalsL,
+ testZeroDurationIntervalsM,
+ testZeroDurationIntervalsN,
+ testZeroDurationIntervalsO
+ ];
+ for (var i = 0; i < tests.length; i++) {
+ var anim = createAnim();
+ svg.setCurrentTime(0);
+ tests[i](anim);
+ removeAnim(anim);
+ }
+ SimpleTest.finish();
+}
+
+function checkSample(time, expectedValue) {
+ svg.setCurrentTime(time);
+ is(circle.cx.animVal.value, expectedValue);
+}
+
+function testZeroDurationIntervalsA(anim) {
+ // The zero-duration interval should play, followed by a second interval
+ // starting at the same point. There is no end for the interval
+ // at 4s so it should not play.
+ anim.setAttribute('begin', '1s ;4s');
+ anim.setAttribute('end', '1s; 2s');
+ anim.setAttribute('dur', '2s ');
+ anim.setAttribute('fill', 'freeze');
+ checkSample(0,-100);
+ checkSample(1,0);
+ checkSample(1.1,5);
+ checkSample(2,50);
+ checkSample(3,50);
+ checkSample(4,50);
+ checkSample(5,50);
+ checkSample(6,50);
+}
+
+function testZeroDurationIntervalsB(anim) {
+ // This interval should however actually restart as there is a valid end-point
+ anim.setAttribute('begin', '1s ;4s');
+ anim.setAttribute('end', '1.1s; indefinite');
+ anim.setAttribute('dur', '2s ');
+ anim.setAttribute('fill', 'freeze');
+ checkSample(0,-100);
+ checkSample(1,0);
+ checkSample(1.1,5);
+ checkSample(2,5);
+ checkSample(4,0);
+ checkSample(5,50);
+}
+
+function testZeroDurationIntervalsC(anim) {
+ // -0.5s has already been used as the endpoint of one interval so don't use it
+ // a second time
+ anim.setAttribute('begin', '-2s; -0.5s');
+ anim.setAttribute('end', '-0.5s; 1s');
+ anim.setAttribute('dur', '2s');
+ anim.setAttribute('fill', 'freeze');
+ checkSample(0,25);
+ checkSample(1.5,75);
+}
+
+function testZeroDurationIntervalsD(anim) {
+ // Two end points that could make a zero-length interval
+ anim.setAttribute('begin', '-2s; -0.5s');
+ anim.setAttribute('end', '-0.5s; -0.5s; 1s');
+ anim.setAttribute('dur', '2s');
+ anim.setAttribute('fill', 'freeze');
+ checkSample(0,25);
+ checkSample(1.5,75);
+}
+
+function testZeroDurationIntervalsE(anim) {
+ // Should give us 1s-1s, 1s-5s
+ anim.setAttribute('begin', '1s');
+ anim.setAttribute('end', '1s; 5s');
+ anim.setAttribute('fill', 'freeze');
+ is(anim.getStartTime(),1);
+ checkSample(0,-100);
+ checkSample(1,0);
+ checkSample(6,40);
+}
+
+function testZeroDurationIntervalsF(anim) {
+ // Should give us 1s-1s
+ anim.setAttribute('begin', '1s');
+ anim.setAttribute('end', '1s');
+ anim.setAttribute('fill', 'freeze');
+ is(anim.getStartTime(),1);
+ checkSample(0,-100);
+ checkSample(1,0);
+ checkSample(2,0);
+ try {
+ anim.getStartTime();
+ ok(false, "Failed to throw exception when there's no current interval.");
+ } catch (e) { }
+}
+
+function testZeroDurationIntervalsG(anim) {
+ // Test a non-zero interval after a zero interval
+ // Should give us 1-2s, 3-3s, 3-4s
+ anim.setAttribute('begin', '1s; 3s');
+ anim.setAttribute('end', '3s; 5s');
+ anim.setAttribute('dur', '1s');
+ anim.setAttribute('fill', 'freeze');
+ checkSample(0,-100);
+ checkSample(1,0);
+ checkSample(2,100);
+ checkSample(3,0);
+ checkSample(5,100);
+}
+
+function testZeroDurationIntervalsH(anim) {
+ // Test multiple non-adjacent zero-intervals
+ // Should give us 1-1s, 1-2s, 3-3s, 3-4s
+ anim.setAttribute('begin', '1s; 3s');
+ anim.setAttribute('end', '1s; 3s; 5s');
+ anim.setAttribute('dur', '1s');
+ anim.setAttribute('fill', 'freeze');
+ checkSample(0,-100);
+ checkSample(1,0);
+ checkSample(2,100);
+ checkSample(3,0);
+ checkSample(5,100);
+}
+
+function testZeroDurationIntervalsI(anim) {
+ // Test skipping values that are the same
+ // Should give us 1-1s, 1-2s
+ anim.setAttribute('begin', '1s; 1s');
+ anim.setAttribute('end', '1s; 1s; 2s');
+ anim.setAttribute('fill', 'freeze');
+ is(anim.getStartTime(),1);
+ checkSample(0,-100);
+ checkSample(1,0);
+ checkSample(2,10);
+ checkSample(3,10);
+}
+
+function testZeroDurationIntervalsJ(anim) {
+ // Should give us 0-0.5s, 1-1s, 1-3s
+ anim.setAttribute('begin', '0s; 1s; 1s');
+ anim.setAttribute('end', '1s; 3s');
+ anim.setAttribute('dur', '0.5s');
+ anim.setAttribute('fill', 'freeze');
+ is(anim.getStartTime(),0);
+ checkSample(0,0);
+ checkSample(0.6,100);
+ checkSample(1,0);
+ checkSample(2,100);
+}
+
+function testZeroDurationIntervalsK(anim) {
+ // Should give us -0.5-1s
+ anim.setAttribute('begin', '-0.5s');
+ anim.setAttribute('end', '-0.5s; 1s');
+ anim.setAttribute('fill', 'freeze');
+ is(anim.getStartTime(),-0.5);
+ checkSample(0,5);
+ checkSample(1,15);
+ checkSample(2,15);
+}
+
+function testZeroDurationIntervalsL(anim) {
+ // Test that multiple end values are ignored
+ // Should give us 1-1s, 1-3s
+ anim.setAttribute('begin', '1s');
+ anim.setAttribute('end', '1s; 1s; 1s; 3s');
+ anim.setAttribute('fill', 'freeze');
+ is(anim.getStartTime(),1);
+ checkSample(0,-100);
+ checkSample(1,0);
+ checkSample(2,10);
+ checkSample(4,20);
+}
+
+function testZeroDurationIntervalsM(anim) {
+ // Test 0-duration interval at start
+ anim.setAttribute('begin', '0s');
+ anim.setAttribute('end', '0s');
+ anim.setAttribute('fill', 'freeze');
+ try {
+ anim.getStartTime();
+ ok(false, "Failed to throw exception when there's no current interval.");
+ } catch (e) { }
+ checkSample(0,0);
+ checkSample(1,0);
+}
+
+function testZeroDurationIntervalsN(anim) {
+ // Test 0-active-duration interval at start (different code path to above)
+ anim.setAttribute('begin', '0s');
+ anim.setAttribute('repeatDur', '0s');
+ anim.setAttribute('fill', 'freeze');
+ try {
+ anim.getStartTime();
+ ok(false, "Failed to throw exception when there's no current interval.");
+ } catch (e) { }
+ checkSample(0,0);
+ checkSample(1,0);
+}
+
+function testZeroDurationIntervalsO(anim) {
+ // Make a zero-duration interval by constraining the active duration
+ // We should not loop infinitely but should look for the next begin time after
+ // that (in this case that is 2s, which would otherwise have been skipped
+ // because restart=whenNotActive)
+ // Should give us 1-1s, 2-2s
+ anim.setAttribute('begin', '1s; 2s');
+ anim.setAttribute('repeatDur', '0s');
+ anim.setAttribute('restart', 'whenNotActive');
+ anim.setAttribute('fill', 'freeze');
+ is(anim.getStartTime(),1);
+ checkSample(0,-100);
+ checkSample(1,0);
+ checkSample(1.5,0);
+ checkSample(3,0);
+ try {
+ anim.getStartTime();
+ ok(false, "Failed to throw exception when there's no current interval.");
+ } catch (e) { }
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Tests updated intervals</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"
+ onload="this.pauseAnimations()">
+ <circle cx="20" cy="20" r="15" fill="blue" id="circle">
+ <animate attributeName="cx" from="0" to="100" begin="2s" dur="4s"
+ id="anim1" attributeType="XML"/>
+ </circle>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Tests for updated intervals **/
+
+/* Global Variables */
+SimpleTest.waitForExplicitFinish();
+
+function main() {
+ var svg = document.getElementById("svg");
+ ok(svg.animationsPaused(), "should be paused by <svg> load handler");
+ is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler");
+
+ var anim = document.getElementById("anim1");
+
+ // Check regular operation
+ svg.setCurrentTime(3);
+ is(anim.getStartTime(), 2, "Unexpected initial start time");
+
+ // Add an instance time before the current interval at t=1s
+ anim.beginElementAt(-2);
+
+ // We shouldn't change the begin time
+ is(anim.getStartTime(), 2, "Start time shouldn't have changed");
+
+ // Or the end--that is, if we go to t=5.5 we should still be running
+ svg.setCurrentTime(5.5);
+ try {
+ is(anim.getSimpleDuration(), 4, "Simple duration shouldn't have changed");
+ is(anim.getStartTime(), 2, "Start time shouldn't have changed after seek");
+ } catch (e) {
+ if (e.name != "InvalidStateError" ||
+ e.code != DOMException.INVALID_STATE_ERR)
+ throw e;
+ ok(false, "Animation ended too early, even though begin time and " +
+ "simple duration didn't change");
+ }
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL values</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=557885">Mozilla Bug
+ 474742</a>
+<p id="display"></p>
+<div id="content">
+<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px">
+ <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/>
+</svg>
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL values **/
+
+var gSvg = document.getElementById("svg");
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ gSvg.pauseAnimations();
+
+ var testCases = Array();
+
+ // Single value
+ testCases.push({
+ 'attr' : { 'values': 'a' },
+ 'times': [ [ 0, 'a' ] ]
+ });
+
+ // The parsing below is based on the following discussion:
+ //
+ // http://lists.w3.org/Archives/Public/www-svg/2011Nov/0136.html
+ //
+ // In summary:
+ // * Values lists are semi-colon delimited and semi-colon terminated.
+ // * However, if there are extra non-whitespace characters after the final
+ // semi-colon then there's an implied semi-colon at the end.
+ //
+ // This differs to what is specified in SVG 1.1 but is consistent with the
+ // majority of browsers and with existing content (particularly that generated
+ // by Ikivo Animator).
+
+ // Trailing semi-colon
+ testCases.push({
+ 'attr' : { 'values': 'a;' },
+ 'times': [ [ 0, 'a' ], [ 10, 'a' ] ]
+ });
+
+ // Trailing semi-colon + whitespace
+ testCases.push({
+ 'attr' : { 'values': 'a; ' },
+ 'times': [ [ 0, 'a' ], [ 10, 'a' ] ]
+ });
+
+ // Whitespace + trailing semi-colon
+ testCases.push({
+ 'attr' : { 'values': 'a ;' },
+ 'times': [ [ 0, 'a' ], [ 10, 'a' ] ]
+ });
+
+ // Empty at end
+ testCases.push({
+ 'attr' : { 'values': 'a;;' },
+ 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, '' ] ]
+ });
+
+ // Empty at end + whitespace
+ testCases.push({
+ 'attr' : { 'values': 'a;; ' },
+ 'times': [ [ 0, 'a' ], [ 4, 'a' ], [ 5, '' ], [ 10, '' ] ]
+ });
+
+ // Empty in middle
+ testCases.push({
+ 'attr' : { 'values': 'a;;b' },
+ 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, 'b' ] ]
+ });
+
+ // Empty in middle + trailing semi-colon
+ testCases.push({
+ 'attr' : { 'values': 'a;;b;' },
+ 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, 'b' ] ]
+ });
+
+ // Whitespace in middle
+ testCases.push({
+ 'attr' : { 'values': 'a; ;b' },
+ 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, 'b' ] ]
+ });
+
+ // Empty at start
+ testCases.push({
+ 'attr' : { 'values': ';a' },
+ 'times': [ [ 0, '' ], [ 5, 'a' ], [ 10, 'a' ] ]
+ });
+
+ // Whitespace at start
+ testCases.push({
+ 'attr' : { 'values': ' ;a' },
+ 'times': [ [ 0, '' ], [ 5, 'a' ], [ 10, 'a' ] ]
+ });
+
+ // Embedded whitespace
+ testCases.push({
+ 'attr' : { 'values': ' a b ; c d ' },
+ 'times': [ [ 0, 'a b' ], [ 5, 'c d' ], [ 10, 'c d' ] ]
+ });
+
+ // Whitespace only
+ testCases.push({
+ 'attr' : { 'values': ' ' },
+ 'times': [ [ 0, '' ], [ 10, '' ] ]
+ });
+
+ for (var i = 0; i < testCases.length; i++) {
+ gSvg.setCurrentTime(0);
+ var test = testCases[i];
+
+ // Create animation elements
+ var anim = createAnim(test.attr);
+
+ // Run samples
+ for (var j = 0; j < test.times.length; j++) {
+ var curSample = test.times[j];
+ gSvg.setCurrentTime(curSample[0]);
+ checkSample(anim, curSample[1], curSample[0], i);
+ }
+
+ anim.parentNode.removeChild(anim);
+ }
+
+ SimpleTest.finish();
+}
+
+function createAnim(attr)
+{
+ const svgns = "http://www.w3.org/2000/svg";
+ var anim = document.createElementNS(svgns, 'animate');
+ anim.setAttribute('attributeName','class');
+ anim.setAttribute('dur','10s');
+ anim.setAttribute('begin','0s');
+ anim.setAttribute('fill','freeze');
+ for (name in attr) {
+ anim.setAttribute(name, attr[name]);
+ }
+ return document.getElementById('circle').appendChild(anim);
+}
+
+function checkSample(anim, expectedValue, sampleTime, caseNum)
+{
+ var msg = "Test case " + caseNum +
+ " (values: '" + anim.getAttribute('values') + "')," +
+ "t=" + sampleTime +
+ ": Unexpected sample value:";
+ is(typeof anim.targetElement.className, "object");
+ is(anim.targetElement.className.animVal, expectedValue, msg);
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>
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 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL Behavior in Data Documents</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="smilTestUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529387">Mozilla Bug 529387</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+/** Test for SMIL Behavior in Data Documents, with XMLHttpRequest **/
+
+SimpleTest.waitForExplicitFinish();
+
+function tryPausing(svg) {
+ // Check that pausing has no effect
+ ok(!svg.animationsPaused(),
+ "shouldn't be paused (because we shouldn't have even started");
+ svg.pauseAnimations();
+ ok(!svg.animationsPaused(), "attempts to pause should have no effect");
+ svg.unpauseAnimations();
+ ok(!svg.animationsPaused(), "still shouldn't be paused, after pause/unpause");
+}
+
+function trySeeking(svg) {
+ // Check that seeking is ineffective
+ is(svg.getCurrentTime(), 0, "should start out at time=0");
+ svg.setCurrentTime(1);
+ is(svg.getCurrentTime(), 0, "shouldn't be able to seek away from time=0");
+}
+
+function tryBeginEnd(anim) {
+ // Check that beginning / ending a particular animation element will trigger
+ // exceptions.
+ var didThrow = false;
+ ok(anim, "need a non-null animate element");
+ try {
+ anim.beginElement();
+ } catch (e) {
+ didThrow = true;
+ }
+ ok(didThrow, "beginElement should fail");
+
+ didThrow = false;
+ try {
+ anim.endElement();
+ } catch (e) {
+ didThrow = true;
+ }
+ ok(didThrow, "endElement should fail");
+}
+
+function main() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", "smilXHR_helper.svg", false);
+ xhr.send();
+ var xdoc = xhr.responseXML;
+
+ var svg = xdoc.getElementById("svg");
+ var circ = xdoc.getElementById("circ");
+ var animXML = xdoc.getElementById("animXML");
+ var animCSS = xdoc.getElementById("animCSS");
+
+ tryPausing(svg);
+ trySeeking(svg);
+ tryBeginEnd(animXML);
+ tryBeginEnd(animCSS);
+
+ // Check that the actual values of our animated attr/prop aren't affected
+ is(circ.cx.animVal.value, circ.cx.baseVal.value,
+ "animation of attribute shouldn't be taking effect");
+ is(SMILUtil.getComputedStyleSimple(circ, "opacity"), "1",
+ "animation of CSS property shouldn't be taking effect");
+
+ SimpleTest.finish();
+}
+
+window.addEventListener("load", main, false);
+]]>
+</script>
+</pre>
+</body>
+</html>