summaryrefslogtreecommitdiffstats
path: root/dom/smil
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 14:29:10 +0000
commit2aa4a82499d4becd2284cdb482213d541b8804dd (patch)
treeb80bf8bf13c3766139fbacc530efd0dd9d54394c /dom/smil
parentInitial commit. (diff)
downloadfirefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz
firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/smil')
-rw-r--r--dom/smil/SMILAnimationController.cpp737
-rw-r--r--dom/smil/SMILAnimationController.h210
-rw-r--r--dom/smil/SMILAnimationFunction.cpp978
-rw-r--r--dom/smil/SMILAnimationFunction.h442
-rw-r--r--dom/smil/SMILAttr.h100
-rw-r--r--dom/smil/SMILBoolType.cpp69
-rw-r--r--dom/smil/SMILBoolType.h47
-rw-r--r--dom/smil/SMILCSSProperty.cpp197
-rw-r--r--dom/smil/SMILCSSProperty.h79
-rw-r--r--dom/smil/SMILCSSValueType.cpp573
-rw-r--r--dom/smil/SMILCSSValueType.h133
-rw-r--r--dom/smil/SMILCompositor.cpp239
-rw-r--r--dom/smil/SMILCompositor.h128
-rw-r--r--dom/smil/SMILCompositorTable.h28
-rw-r--r--dom/smil/SMILEnumType.cpp69
-rw-r--r--dom/smil/SMILEnumType.h47
-rw-r--r--dom/smil/SMILFloatType.cpp81
-rw-r--r--dom/smil/SMILFloatType.h47
-rw-r--r--dom/smil/SMILInstanceTime.cpp188
-rw-r--r--dom/smil/SMILInstanceTime.h166
-rw-r--r--dom/smil/SMILIntegerType.cpp86
-rw-r--r--dom/smil/SMILIntegerType.h42
-rw-r--r--dom/smil/SMILInterval.cpp137
-rw-r--r--dom/smil/SMILInterval.h86
-rw-r--r--dom/smil/SMILKeySpline.cpp127
-rw-r--r--dom/smil/SMILKeySpline.h107
-rw-r--r--dom/smil/SMILMilestone.h75
-rw-r--r--dom/smil/SMILNullType.cpp57
-rw-r--r--dom/smil/SMILNullType.h47
-rw-r--r--dom/smil/SMILParserUtils.cpp633
-rw-r--r--dom/smil/SMILParserUtils.h89
-rw-r--r--dom/smil/SMILRepeatCount.cpp14
-rw-r--r--dom/smil/SMILRepeatCount.h62
-rw-r--r--dom/smil/SMILSetAnimationFunction.cpp79
-rw-r--r--dom/smil/SMILSetAnimationFunction.h67
-rw-r--r--dom/smil/SMILStringType.cpp75
-rw-r--r--dom/smil/SMILStringType.h47
-rw-r--r--dom/smil/SMILTargetIdentifier.h87
-rw-r--r--dom/smil/SMILTimeContainer.cpp301
-rw-r--r--dom/smil/SMILTimeContainer.h299
-rw-r--r--dom/smil/SMILTimeValue.cpp42
-rw-r--r--dom/smil/SMILTimeValue.h131
-rw-r--r--dom/smil/SMILTimeValueSpec.cpp370
-rw-r--r--dom/smil/SMILTimeValueSpec.h143
-rw-r--r--dom/smil/SMILTimeValueSpecParams.h58
-rw-r--r--dom/smil/SMILTimedElement.cpp2180
-rw-r--r--dom/smil/SMILTimedElement.h649
-rw-r--r--dom/smil/SMILType.h212
-rw-r--r--dom/smil/SMILTypes.h30
-rw-r--r--dom/smil/SMILValue.cpp142
-rw-r--r--dom/smil/SMILValue.h75
-rw-r--r--dom/smil/TimeEvent.cpp60
-rw-r--r--dom/smil/TimeEvent.h63
-rw-r--r--dom/smil/crashtests/1010681-1.svg23
-rw-r--r--dom/smil/crashtests/1322770-1.svg3
-rw-r--r--dom/smil/crashtests/1322849-1.svg2
-rw-r--r--dom/smil/crashtests/1343357-1.html12
-rw-r--r--dom/smil/crashtests/1375596-1.svg3
-rw-r--r--dom/smil/crashtests/1402547-1.html3
-rw-r--r--dom/smil/crashtests/1411963-1.html10
-rw-r--r--dom/smil/crashtests/1413319-1.html2
-rw-r--r--dom/smil/crashtests/1535388-1.html18
-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/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-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.html97
-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/849593-1.xhtml34
-rw-r--r--dom/smil/crashtests/crashtests.list61
-rw-r--r--dom/smil/moz.build74
-rw-r--r--dom/smil/test/.eslintrc.js5
-rw-r--r--dom/smil/test/db_smilAnimateMotion.js309
-rw-r--r--dom/smil/test/db_smilCSSFromBy.js207
-rw-r--r--dom/smil/test/db_smilCSSFromTo.js625
-rw-r--r--dom/smil/test/db_smilCSSPaced.js356
-rw-r--r--dom/smil/test/db_smilCSSPropertyList.js104
-rw-r--r--dom/smil/test/db_smilMappedAttrList.js148
-rw-r--r--dom/smil/test/file_smilWithTransition.html79
-rw-r--r--dom/smil/test/mochitest.ini63
-rw-r--r--dom/smil/test/smilAnimateMotionValueLists.js116
-rw-r--r--dom/smil/test/smilExtDoc_helper.svg7
-rw-r--r--dom/smil/test/smilTestUtils.js1014
-rw-r--r--dom/smil/test/smilXHR_helper.svg8
-rw-r--r--dom/smil/test/test_smilAccessKey.xhtml79
-rw-r--r--dom/smil/test/test_smilAdditionFallback.html30
-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.html93
-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.xhtml229
-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.xhtml99
-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_smilWithTransition.html18
-rw-r--r--dom/smil/test/test_smilWithXlink.xhtml48
-rw-r--r--dom/smil/test/test_smilXHR.xhtml88
176 files changed, 21561 insertions, 0 deletions
diff --git a/dom/smil/SMILAnimationController.cpp b/dom/smil/SMILAnimationController.cpp
new file mode 100644
index 0000000000..138cedb20e
--- /dev/null
+++ b/dom/smil/SMILAnimationController.cpp
@@ -0,0 +1,737 @@
+/* -*- 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 "SMILAnimationController.h"
+
+#include <algorithm>
+
+#include "mozilla/AutoRestore.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/PresShellInlines.h"
+#include "mozilla/RestyleManager.h"
+#include "mozilla/SMILTimedElement.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/SVGAnimationElement.h"
+#include "nsContentUtils.h"
+#include "nsCSSProps.h"
+#include "nsRefreshDriver.h"
+#include "mozilla/dom/Document.h"
+#include "SMILCompositor.h"
+#include "SMILCSSProperty.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// SMILAnimationController implementation
+
+//----------------------------------------------------------------------
+// ctors, dtors, factory methods
+
+SMILAnimationController::SMILAnimationController(Document* 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();
+}
+
+SMILAnimationController::~SMILAnimationController() {
+ 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 SMILAnimationController::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 & SMILTimeContainer::PAUSE_PAGEHIDE,
+ "Expecting to be paused for pagehide before disconnect");
+
+ StopSampling(GetRefreshDriver());
+
+ mDocument = nullptr; // (raw pointer)
+}
+
+//----------------------------------------------------------------------
+// SMILTimeContainer methods:
+
+void SMILAnimationController::Pause(uint32_t aType) {
+ SMILTimeContainer::Pause(aType);
+
+ if (mPauseState) {
+ mDeferredStartSampling = false;
+ StopSampling(GetRefreshDriver());
+ }
+}
+
+void SMILAnimationController::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();
+
+ SMILTimeContainer::Resume(aType);
+
+ if (wasPaused && !mPauseState && !mChildContainerTable.IsEmpty()) {
+ MaybeStartSampling(GetRefreshDriver());
+ Sample(); // Run the first sample manually
+ }
+}
+
+SMILTime SMILAnimationController::GetParentTime() const {
+ return (SMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds();
+}
+
+//----------------------------------------------------------------------
+// nsARefreshObserver methods:
+NS_IMPL_ADDREF(SMILAnimationController)
+NS_IMPL_RELEASE(SMILAnimationController)
+
+// nsRefreshDriver Callback function
+void SMILAnimationController::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;
+
+ SMILTime elapsedTime =
+ (SMILTime)(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 =
+ (SMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING +
+ mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING));
+ }
+ mCurrentSampleTime = aTime;
+
+ Sample();
+}
+
+//----------------------------------------------------------------------
+// Animation element registration methods:
+
+void SMILAnimationController::RegisterAnimationElement(
+ SVGAnimationElement* aAnimationElement) {
+ mAnimationElementTable.PutEntry(aAnimationElement);
+ if (mDeferredStartSampling) {
+ mDeferredStartSampling = false;
+ if (!mChildContainerTable.IsEmpty()) {
+ // 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 SMILAnimationController::UnregisterAnimationElement(
+ SVGAnimationElement* aAnimationElement) {
+ mAnimationElementTable.RemoveEntry(aAnimationElement);
+}
+
+//----------------------------------------------------------------------
+// Page show/hide
+
+void SMILAnimationController::OnPageShow() {
+ Resume(SMILTimeContainer::PAUSE_PAGEHIDE);
+}
+
+void SMILAnimationController::OnPageHide() {
+ Pause(SMILTimeContainer::PAUSE_PAGEHIDE);
+}
+
+//----------------------------------------------------------------------
+// Cycle-collection support
+
+void SMILAnimationController::Traverse(
+ nsCycleCollectionTraversalCallback* aCallback) {
+ // Traverse last compositor table
+ if (mLastCompositorTable) {
+ for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) {
+ SMILCompositor* compositor = iter.Get();
+ compositor->Traverse(aCallback);
+ }
+ }
+}
+
+void SMILAnimationController::Unlink() { mLastCompositorTable = nullptr; }
+
+//----------------------------------------------------------------------
+// Refresh driver lifecycle related methods
+
+void SMILAnimationController::NotifyRefreshDriverCreated(
+ nsRefreshDriver* aRefreshDriver) {
+ if (!mPauseState && !mChildContainerTable.IsEmpty()) {
+ MaybeStartSampling(aRefreshDriver);
+ }
+}
+
+void SMILAnimationController::NotifyRefreshDriverDestroying(
+ nsRefreshDriver* aRefreshDriver) {
+ if (!mPauseState && !mDeferredStartSampling) {
+ StopSampling(aRefreshDriver);
+ }
+}
+
+//----------------------------------------------------------------------
+// Timer-related implementation helpers
+
+void SMILAnimationController::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, FlushType::Style,
+ "SMIL animations");
+ mRegisteredWithRefreshDriver = true;
+ }
+}
+
+void SMILAnimationController::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, FlushType::Style);
+ mRegisteredWithRefreshDriver = false;
+ }
+}
+
+void SMILAnimationController::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 SMILAnimationController::DoSample() {
+ DoSample(true); // Skip unchanged time containers
+}
+
+void SMILAnimationController::DoSample(bool aSkipUnchangedContainers) {
+ if (!mDocument) {
+ NS_ERROR("Shouldn't be sampling after document has disconnected");
+ return;
+ }
+ if (mRunningSample) {
+ NS_ERROR("Shouldn't be recursively sampling");
+ return;
+ }
+
+ bool isStyleFlushNeeded = mResampleNeeded;
+ mResampleNeeded = false;
+
+ nsCOMPtr<Document> 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()) {
+ SMILTimeContainer* container = iter.Get()->GetKey();
+ if (!container) {
+ continue;
+ }
+
+ if (!container->IsPausedByType(SMILTimeContainer::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 (SMILAnimationFunction).
+ //
+ // (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
+ UniquePtr<SMILCompositorTable> currentCompositorTable(
+ new SMILCompositorTable(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.get(),
+ 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()) {
+ SMILCompositor* compositor = iter.Get();
+ SMILCompositor* 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(FlushType::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 = std::move(currentCompositorTable);
+ mMightHavePendingStyleUpdates = mightHavePendingStyleUpdates;
+
+ NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!");
+}
+
+void SMILAnimationController::RewindElements() {
+ bool rewindNeeded = false;
+ for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) {
+ SMILTimeContainer* 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();
+ SMILTimeContainer* timeContainer = animElem->GetTimeContainer();
+ if (timeContainer && timeContainer->NeedsRewind()) {
+ animElem->TimedElement().Rewind();
+ }
+ }
+
+ for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) {
+ iter.Get()->GetKey()->ClearNeedsRewind();
+ }
+}
+
+void SMILAnimationController::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.
+
+ SMILTime 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.
+ SMILMilestone nextMilestone(GetCurrentTimeAsSMILTime() + 1, true);
+ for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) {
+ SMILTimeContainer* container = iter.Get()->GetKey();
+ if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) {
+ continue;
+ }
+ SMILMilestone thisMilestone;
+ bool didGetMilestone =
+ container->GetNextMilestoneInParentTime(thisMilestone);
+ if (didGetMilestone && thisMilestone < nextMilestone) {
+ nextMilestone = thisMilestone;
+ }
+ }
+
+ if (nextMilestone.mTime > GetCurrentTimeAsSMILTime()) {
+ break;
+ }
+
+ nsTArray<RefPtr<mozilla::dom::SVGAnimationElement>> elements;
+ for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) {
+ SMILTimeContainer* container = iter.Get()->GetKey();
+ if (container->IsPausedByType(SMILTimeContainer::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");
+ SMILTimeContainer* container = elem->GetTimeContainer();
+ if (!container)
+ // The container may be nullptr if the element has been detached from
+ // its parent since registering a milestone.
+ continue;
+
+ SMILTimeValue containerTimeValue =
+ container->ParentToContainerTime(sampleTime);
+ if (!containerTimeValue.IsDefinite()) continue;
+
+ // Clamp the converted container time to non-negative values.
+ SMILTime containerTime =
+ std::max<SMILTime>(0, containerTimeValue.GetMillis());
+
+ if (nextMilestone.mIsEnd) {
+ elem->TimedElement().SampleEndAt(containerTime);
+ } else {
+ elem->TimedElement().SampleAt(containerTime);
+ }
+ }
+ }
+}
+
+/*static*/
+void SMILAnimationController::SampleTimedElement(
+ SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) {
+ SMILTimeContainer* 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;
+
+ SMILTime containerTime = timeContainer->GetCurrentTimeAsSMILTime();
+
+ MOZ_ASSERT(!timeContainer->IsSeeking(),
+ "Doing a regular sample but the time container is still seeking");
+ aElement->TimedElement().SampleAt(containerTime);
+}
+
+/*static*/
+void SMILAnimationController::AddAnimationToCompositorTable(
+ SVGAnimationElement* aElement, SMILCompositorTable* aCompositorTable,
+ bool& aStyleFlushNeeded) {
+ // Add a compositor to the hash table if there's not already one there
+ SMILTargetIdentifier key;
+ if (!GetTargetIdentifierForAnimation(aElement, key))
+ // Something's wrong/missing about animation's target; skip this animation
+ return;
+
+ SMILAnimationFunction& 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.
+ SMILCompositor* 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.
+ SMILCompositor* 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,
+ nsAtom* 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 SMILTargetIdentifier
+// for this target.
+/*static*/
+bool SMILAnimationController::GetTargetIdentifierForAnimation(
+ SVGAnimationElement* aAnimElem, SMILTargetIdentifier& 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.
+ RefPtr<nsAtom> 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;
+
+ // Construct the key
+ aResult.mElement = targetElem;
+ aResult.mAttributeName = attributeName;
+ aResult.mAttributeNamespaceID = attributeNamespaceID;
+
+ return true;
+}
+
+bool SMILAnimationController::PreTraverse() {
+ return PreTraverseInSubtree(nullptr);
+}
+
+bool SMILAnimationController::PreTraverseInSubtree(Element* aRoot) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mMightHavePendingStyleUpdates) {
+ return false;
+ }
+
+ nsPresContext* context = mDocument->GetPresContext();
+ if (!context) {
+ return false;
+ }
+
+ bool foundElementsNeedingRestyle = false;
+ for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) {
+ SVGAnimationElement* animElement = iter.Get()->GetKey();
+
+ SMILTargetIdentifier key;
+ if (!GetTargetIdentifierForAnimation(animElement, key)) {
+ // Something's wrong/missing about animation's target; skip this animation
+ continue;
+ }
+
+ // Ignore restyles that aren't in the flattened tree subtree rooted at
+ // aRoot.
+ if (aRoot && !nsContentUtils::ContentIsFlattenedTreeDescendantOf(
+ key.mElement, aRoot)) {
+ continue;
+ }
+
+ context->RestyleManager()->PostRestyleEventForAnimations(
+ key.mElement, PseudoStyleType::NotPseudo, RestyleHint::RESTYLE_SMIL);
+
+ foundElementsNeedingRestyle = true;
+ }
+
+ // Only clear the mMightHavePendingStyleUpdates flag if we definitely posted
+ // all restyles.
+ if (!aRoot) {
+ mMightHavePendingStyleUpdates = false;
+ }
+
+ return foundElementsNeedingRestyle;
+}
+
+//----------------------------------------------------------------------
+// Add/remove child time containers
+
+nsresult SMILAnimationController::AddChild(SMILTimeContainer& 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 SMILAnimationController::RemoveChild(SMILTimeContainer& aChild) {
+ mChildContainerTable.RemoveEntry(&aChild);
+
+ if (!mPauseState && mChildContainerTable.IsEmpty()) {
+ StopSampling(GetRefreshDriver());
+ }
+}
+
+// Helper method
+nsRefreshDriver* SMILAnimationController::GetRefreshDriver() {
+ if (!mDocument) {
+ NS_ERROR("Requesting refresh driver after document has disconnected!");
+ return nullptr;
+ }
+
+ nsPresContext* context = mDocument->GetPresContext();
+ return context ? context->RefreshDriver() : nullptr;
+}
+
+void SMILAnimationController::FlagDocumentNeedsFlush() {
+ if (PresShell* presShell = mDocument->GetPresShell()) {
+ presShell->SetNeedStyleFlush();
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILAnimationController.h b/dom/smil/SMILAnimationController.h
new file mode 100644
index 0000000000..20abeb32aa
--- /dev/null
+++ b/dom/smil/SMILAnimationController.h
@@ -0,0 +1,210 @@
+/* -*- 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 DOM_SMIL_SMILANIMATIONCONTROLLER_H_
+#define DOM_SMIL_SMILANIMATIONCONTROLLER_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILCompositorTable.h"
+#include "mozilla/SMILMilestone.h"
+#include "mozilla/SMILTimeContainer.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsTHashtable.h"
+#include "nsHashKeys.h"
+#include "nsRefreshObservers.h"
+
+class nsRefreshDriver;
+
+namespace mozilla {
+struct SMILTargetIdentifier;
+namespace dom {
+class Element;
+class SVGAnimationElement;
+} // namespace dom
+
+//----------------------------------------------------------------------
+// SMILAnimationController
+//
+// 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 document 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 SMILAnimationController final : public SMILTimeContainer,
+ public nsARefreshObserver {
+ public:
+ explicit SMILAnimationController(mozilla::dom::Document* aDoc);
+
+ // Clears mDocument pointer. (Called by our mozilla::dom::Document when it's
+ // going away)
+ void Disconnect();
+
+ // SMILContainer
+ virtual void Pause(uint32_t aType) override;
+ virtual void Resume(uint32_t aType) override;
+ virtual SMILTime 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;
+ }
+
+ bool MightHavePendingStyleUpdates() const {
+ return mMightHavePendingStyleUpdates;
+ }
+
+ bool PreTraverse();
+ bool PreTraverseInSubtree(mozilla::dom::Element* aRoot);
+
+ protected:
+ ~SMILAnimationController();
+
+ // alias declarations
+ using TimeContainerPtrKey = nsPtrHashKey<SMILTimeContainer>;
+ using TimeContainerHashtable = nsTHashtable<TimeContainerPtrKey>;
+ using AnimationElementPtrKey = nsPtrHashKey<dom::SVGAnimationElement>;
+ using AnimationElementHashtable = nsTHashtable<AnimationElementPtrKey>;
+
+ // 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,
+ SMILCompositorTable* aCompositorTable, bool& aStyleFlushNeeded);
+
+ static bool GetTargetIdentifierForAnimation(
+ mozilla::dom::SVGAnimationElement* aAnimElem,
+ SMILTargetIdentifier& aResult);
+
+ // Methods for adding/removing time containers
+ virtual nsresult AddChild(SMILTimeContainer& aChild) override;
+ virtual void RemoveChild(SMILTimeContainer& 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).
+ SMILTime 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
+ mozilla::dom::Document* 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)
+ UniquePtr<SMILCompositorTable> mLastCompositorTable;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILANIMATIONCONTROLLER_H_
diff --git a/dom/smil/SMILAnimationFunction.cpp b/dom/smil/SMILAnimationFunction.cpp
new file mode 100644
index 0000000000..43b470896e
--- /dev/null
+++ b/dom/smil/SMILAnimationFunction.cpp
@@ -0,0 +1,978 @@
+/* -*- 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 "SMILAnimationFunction.h"
+
+#include <math.h>
+
+#include <algorithm>
+#include <utility>
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/SMILAttr.h"
+#include "mozilla/SMILCSSValueType.h"
+#include "mozilla/SMILNullType.h"
+#include "mozilla/SMILParserUtils.h"
+#include "mozilla/SMILTimedElement.h"
+#include "mozilla/dom/SVGAnimationElement.h"
+#include "nsAttrValueInlines.h"
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+#include "nsIContent.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// Static members
+
+nsAttrValue::EnumTable SMILAnimationFunction::sAccumulateTable[] = {
+ {"none", false}, {"sum", true}, {nullptr, 0}};
+
+nsAttrValue::EnumTable SMILAnimationFunction::sAdditiveTable[] = {
+ {"replace", false}, {"sum", true}, {nullptr, 0}};
+
+nsAttrValue::EnumTable SMILAnimationFunction::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.
+
+SMILAnimationFunction::SMILAnimationFunction()
+ : 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 SMILAnimationFunction::SetAnimationElement(
+ SVGAnimationElement* aAnimationElement) {
+ mAnimationElement = aAnimationElement;
+}
+
+bool SMILAnimationFunction::SetAttr(nsAtom* 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 SMILAnimationFunction::UnsetAttr(nsAtom* 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 SMILAnimationFunction::SampleAt(SMILTime aSampleTime,
+ const SMILTimeValue& 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 SMILAnimationFunction::SampleLastValue(uint32_t aRepeatIteration) {
+ if (mHasChanged || !mLastValue || mRepeatIteration != aRepeatIteration) {
+ mHasChanged = true;
+ }
+
+ mRepeatIteration = aRepeatIteration;
+ mLastValue = true;
+}
+
+void SMILAnimationFunction::Activate(SMILTime aBeginTime) {
+ mBeginTime = aBeginTime;
+ mIsActive = true;
+ mIsFrozen = false;
+ mHasChanged = true;
+}
+
+void SMILAnimationFunction::Inactivate(bool aIsFrozen) {
+ mIsActive = false;
+ mIsFrozen = aIsFrozen;
+ mHasChanged = true;
+}
+
+void SMILAnimationFunction::ComposeResult(const SMILAttr& aSMILAttr,
+ SMILValue& aResult) {
+ mHasChanged = false;
+ mPrevSampleWasSingleValueAnimation = false;
+ mWasSkippedInPrevSample = false;
+
+ // Skip animations that are inactive or in error
+ if (!IsActiveOrFrozen() || mErrorFlags != 0) return;
+
+ // Get the animation values
+ SMILValueArray 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;
+
+ SMILValue result;
+
+ if (values.Length() == 1 && !IsToAnimation()) {
+ // Single-valued animation
+ result = values[0];
+ mPrevSampleWasSingleValueAnimation = true;
+
+ } else if (mLastValue) {
+ // Sampling last value
+ const SMILValue& 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 = std::move(result);
+ }
+}
+
+int8_t SMILAnimationFunction::CompareTo(
+ const SMILAnimationFunction* 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 SMILTimedElement& thisTimedElement = mAnimationElement->TimedElement();
+ const SMILTimedElement& 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 SMILAnimationFunction::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 SMILAnimationFunction::HasChanged() const {
+ return mHasChanged || mValueNeedsReparsingEverySample;
+}
+
+bool SMILAnimationFunction::UpdateCachedTarget(
+ const SMILTargetIdentifier& aNewTarget) {
+ if (!mLastTarget.Equals(aNewTarget)) {
+ mLastTarget = aNewTarget;
+ return true;
+ }
+ return false;
+}
+
+//----------------------------------------------------------------------
+// Implementation helpers
+
+nsresult SMILAnimationFunction::InterpolateResult(const SMILValueArray& aValues,
+ SMILValue& aResult,
+ SMILValue& 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()) {
+ SMILTime 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;
+ SMILCalcMode calcMode = GetCalcMode();
+
+ // Force discrete calcMode for visibility since StyleAnimationValue will
+ // try to interpolate it using the special clamping behavior defined for
+ // CSS.
+ if (SMILCSSValueType::PropertyFromValue(aValues[0]) ==
+ eCSSProperty_visibility) {
+ calcMode = CALC_DISCRETE;
+ }
+
+ if (calcMode != CALC_DISCRETE) {
+ // Get the normalised progress between adjacent values
+ const SMILValue* from = nullptr;
+ const SMILValue* 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];
+
+ // For animation of CSS properties, normally when interpolating we perform
+ // a zero-value fixup which means that empty values (values with type
+ // SMILCSSValueType but a null pointer value) are converted into
+ // a suitable zero value based on whatever they're being interpolated
+ // with. For discrete animation, however, since we don't interpolate,
+ // that never happens. In some rare cases, such as discrete non-additive
+ // by-animation, we can arrive here with |aResult| being such an empty
+ // value so we need to manually perform the fixup.
+ //
+ // We could define a generic method for this on SMILValue but its faster
+ // and simpler to just special case SMILCSSValueType.
+ if (aResult.mType == &SMILCSSValueType::sSingleton) {
+ // We have currently only ever encountered this case for the first
+ // value of a by-animation (which has two values) and since we have no
+ // way of testing other cases we just skip them (but assert if we
+ // ever do encounter them so that we can add code to handle them).
+ if (index + 1 >= aValues.Length()) {
+ MOZ_ASSERT(aResult.mU.mPtr, "The last value should not be empty");
+ } else {
+ // Base the type of the zero value on the next element in the series.
+ SMILCSSValueType::FinalizeValue(aResult, aValues[index + 1]);
+ }
+ }
+ }
+ rv = NS_OK;
+ }
+ return rv;
+}
+
+nsresult SMILAnimationFunction::AccumulateResult(const SMILValueArray& aValues,
+ SMILValue& aResult) {
+ if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) {
+ const SMILValue& 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 SMILAnimationFunction::ComputePacedPosition(
+ const SMILValueArray& aValues, double aSimpleProgress,
+ double& aIntervalProgress, const SMILValue*& aFrom, const SMILValue*& 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;
+
+ DebugOnly<nsresult> rv =
+ 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;
+ }
+ }
+
+ MOZ_ASSERT_UNREACHABLE(
+ "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 SMILAnimationFunction::ComputePacedTotalDistance(
+ const SMILValueArray& 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 SMILAnimationFunction::ScaleSimpleProgress(double aProgress,
+ SMILCalcMode 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 SMILAnimationFunction::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");
+
+ SMILKeySpline const& spline = mKeySplines[aIntervalIndex];
+ return spline.GetSplineValue(aProgress);
+}
+
+bool SMILAnimationFunction::HasAttr(nsAtom* aAttName) const {
+ return mAnimationElement->HasAttr(aAttName);
+}
+
+const nsAttrValue* SMILAnimationFunction::GetAttr(nsAtom* aAttName) const {
+ return mAnimationElement->GetParsedAttr(aAttName);
+}
+
+bool SMILAnimationFunction::GetAttr(nsAtom* aAttName,
+ nsAString& aResult) const {
+ return mAnimationElement->GetAttr(aAttName, aResult);
+}
+
+/*
+ * A utility function to make querying an attribute that corresponds to an
+ * SMILValue 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 SMILValue.
+ * @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 SMILAttr::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 SMILAnimationFunction::ParseAttr(nsAtom* aAttName,
+ const SMILAttr& aSMILAttr,
+ SMILValue& 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 SMILAnimationFunction::GetValues(const SMILAttr& aSMILAttr,
+ SMILValueArray& aResult) {
+ if (!mAnimationElement) return NS_ERROR_FAILURE;
+
+ mValueNeedsReparsingEverySample = false;
+ SMILValueArray result;
+
+ // If "values" is set, use it
+ if (HasAttr(nsGkAtoms::values)) {
+ nsAutoString attValue;
+ GetAttr(nsGkAtoms::values, attValue);
+ bool preventCachingOfSandwich = false;
+ if (!SMILParserUtils::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;
+ SMILValue 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, fallible)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // AppendElement() below must succeed, because SetCapacity() succeeded.
+ if (!to.IsNull()) {
+ if (!from.IsNull()) {
+ MOZ_ALWAYS_TRUE(result.AppendElement(from, fallible));
+ MOZ_ALWAYS_TRUE(result.AppendElement(to, fallible));
+ } else {
+ MOZ_ALWAYS_TRUE(result.AppendElement(to, fallible));
+ }
+ } else if (!by.IsNull()) {
+ SMILValue effectiveFrom(by.mType);
+ if (!from.IsNull()) effectiveFrom = from;
+ // Set values to 'from; from + by'
+ MOZ_ALWAYS_TRUE(result.AppendElement(effectiveFrom, fallible));
+ SMILValue effectiveTo(effectiveFrom);
+ if (!effectiveTo.IsNull() && NS_SUCCEEDED(effectiveTo.Add(by))) {
+ MOZ_ALWAYS_TRUE(result.AppendElement(effectiveTo, 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;
+ }
+ }
+
+ aResult = std::move(result);
+
+ return NS_OK;
+}
+
+void SMILAnimationFunction::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 SMILAnimationFunction::CheckKeyTimes(uint32_t aNumValues) {
+ if (!HasAttr(nsGkAtoms::keyTimes)) return;
+
+ SMILCalcMode 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 SMILAnimationFunction::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 SMILAnimationFunction::IsValueFixedForSimpleDuration() const {
+ return mSimpleDuration.IsIndefinite() ||
+ (!mHasChanged && mPrevSampleWasSingleValueAnimation);
+}
+
+//----------------------------------------------------------------------
+// Property getters
+
+bool SMILAnimationFunction::GetAccumulate() const {
+ const nsAttrValue* value = GetAttr(nsGkAtoms::accumulate);
+ if (!value) return false;
+
+ return value->GetEnumValue();
+}
+
+bool SMILAnimationFunction::GetAdditive() const {
+ const nsAttrValue* value = GetAttr(nsGkAtoms::additive);
+ if (!value) return false;
+
+ return value->GetEnumValue();
+}
+
+SMILAnimationFunction::SMILCalcMode SMILAnimationFunction::GetCalcMode() const {
+ const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode);
+ if (!value) return CALC_LINEAR;
+
+ return SMILCalcMode(value->GetEnumValue());
+}
+
+//----------------------------------------------------------------------
+// Property setters / un-setters:
+
+nsresult SMILAnimationFunction::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 SMILAnimationFunction::UnsetAccumulate() {
+ SetAccumulateErrorFlag(false);
+ mHasChanged = true;
+}
+
+nsresult SMILAnimationFunction::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 SMILAnimationFunction::UnsetAdditive() {
+ SetAdditiveErrorFlag(false);
+ mHasChanged = true;
+}
+
+nsresult SMILAnimationFunction::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 SMILAnimationFunction::UnsetCalcMode() {
+ SetCalcModeErrorFlag(false);
+ mHasChanged = true;
+}
+
+nsresult SMILAnimationFunction::SetKeySplines(const nsAString& aKeySplines,
+ nsAttrValue& aResult) {
+ mKeySplines.Clear();
+ aResult.SetTo(aKeySplines);
+
+ mHasChanged = true;
+
+ if (!SMILParserUtils::ParseKeySplines(aKeySplines, mKeySplines)) {
+ mKeySplines.Clear();
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+void SMILAnimationFunction::UnsetKeySplines() {
+ mKeySplines.Clear();
+ SetKeySplinesErrorFlag(false);
+ mHasChanged = true;
+}
+
+nsresult SMILAnimationFunction::SetKeyTimes(const nsAString& aKeyTimes,
+ nsAttrValue& aResult) {
+ mKeyTimes.Clear();
+ aResult.SetTo(aKeyTimes);
+
+ mHasChanged = true;
+
+ if (!SMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyTimes, true,
+ mKeyTimes)) {
+ mKeyTimes.Clear();
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+void SMILAnimationFunction::UnsetKeyTimes() {
+ mKeyTimes.Clear();
+ SetKeyTimesErrorFlag(false);
+ mHasChanged = true;
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILAnimationFunction.h b/dom/smil/SMILAnimationFunction.h
new file mode 100644
index 0000000000..9a12f1a846
--- /dev/null
+++ b/dom/smil/SMILAnimationFunction.h
@@ -0,0 +1,442 @@
+/* -*- 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 DOM_SMIL_SMILANIMATIONFUNCTION_H_
+#define DOM_SMIL_SMILANIMATIONFUNCTION_H_
+
+#include "mozilla/SMILAttr.h"
+#include "mozilla/SMILKeySpline.h"
+#include "mozilla/SMILTargetIdentifier.h"
+#include "mozilla/SMILTimeValue.h"
+#include "mozilla/SMILTypes.h"
+#include "mozilla/SMILValue.h"
+#include "nsAttrValue.h"
+#include "nsGkAtoms.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace dom {
+class SVGAnimationElement;
+} // namespace dom
+
+//----------------------------------------------------------------------
+// SMILAnimationFunction
+//
+// 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 SMILAnimationFunction {
+ public:
+ SMILAnimationFunction();
+
+ /*
+ * 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(nsAtom* 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(nsAtom* 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(SMILTime aSampleTime, const SMILTimeValue& 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(SMILTime 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 SMILAttr& aSMILAttr, SMILValue& 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 SMILAnimationFunction* 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 SMILValues (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 SMILCompositor 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 SMILTargetIdentifier 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 SMILTargetIdentifier& 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 SMILAnimationFunctions
+ class Comparator {
+ public:
+ bool Equals(const SMILAnimationFunction* aElem1,
+ const SMILAnimationFunction* aElem2) const {
+ return (aElem1->CompareTo(aElem2) == 0);
+ }
+ bool LessThan(const SMILAnimationFunction* aElem1,
+ const SMILAnimationFunction* aElem2) const {
+ return (aElem1->CompareTo(aElem2) < 0);
+ }
+ };
+
+ protected:
+ // alias declarations
+ using SMILValueArray = FallibleTArray<SMILValue>;
+
+ // Types
+ enum SMILCalcMode : uint8_t {
+ CALC_LINEAR,
+ CALC_DISCRETE,
+ CALC_PACED,
+ CALC_SPLINE
+ };
+
+ // Used for sorting SMILAnimationFunctions
+ SMILTime GetBeginTime() const { return mBeginTime; }
+
+ // Property getters
+ bool GetAccumulate() const;
+ bool GetAdditive() const;
+ virtual SMILCalcMode 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 SMILValueArray& aValues,
+ SMILValue& aResult, SMILValue& aBaseValue);
+ nsresult AccumulateResult(const SMILValueArray& aValues, SMILValue& aResult);
+
+ nsresult ComputePacedPosition(const SMILValueArray& aValues,
+ double aSimpleProgress,
+ double& aIntervalProgress,
+ const SMILValue*& aFrom, const SMILValue*& aTo);
+ double ComputePacedTotalDistance(const SMILValueArray& aValues) const;
+
+ /**
+ * Adjust the simple progress, that is, the point within the simple duration,
+ * by applying any keyTimes.
+ */
+ double ScaleSimpleProgress(double aProgress, SMILCalcMode 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(nsAtom* aAttName) const;
+ virtual const nsAttrValue* GetAttr(nsAtom* aAttName) const;
+ virtual bool GetAttr(nsAtom* aAttName, nsAString& aResult) const;
+
+ bool ParseAttr(nsAtom* aAttName, const SMILAttr& aSMILAttr,
+ SMILValue& aResult, bool& aPreventCachingOfSandwich) const;
+
+ virtual nsresult GetValues(const SMILAttr& aSMILAttr,
+ SMILValueArray& 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<SMILKeySpline> 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.
+ SMILTime mSampleTime; // sample time within simple dur
+ SMILTimeValue mSimpleDuration;
+ uint32_t mRepeatIteration;
+
+ SMILTime 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 SMILAnimationFunction 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).
+ SMILWeakTargetIdentifier mLastTarget;
+
+ // Boolean flags
+ bool mIsActive : 1;
+ bool mIsFrozen : 1;
+ bool mLastValue : 1;
+ bool mHasChanged : 1;
+ bool mValueNeedsReparsingEverySample : 1;
+ bool mPrevSampleWasSingleValueAnimation : 1;
+ bool mWasSkippedInPrevSample : 1;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILANIMATIONFUNCTION_H_
diff --git a/dom/smil/SMILAttr.h b/dom/smil/SMILAttr.h
new file mode 100644
index 0000000000..a67921a9ad
--- /dev/null
+++ b/dom/smil/SMILAttr.h
@@ -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/. */
+
+#ifndef DOM_SMIL_SMILATTR_H_
+#define DOM_SMIL_SMILATTR_H_
+
+#include "nscore.h"
+#include "nsStringFwd.h"
+
+class nsIContent;
+
+namespace mozilla {
+
+class SMILValue;
+
+namespace dom {
+class SVGAnimationElement;
+} // namespace dom
+
+////////////////////////////////////////////////////////////////////////
+// SMILAttr: 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 SMILAttr {
+ public:
+ /**
+ * Creates a new SMILValue 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, SMILValue& aValue,
+ bool& aPreventCachingOfSandwich) const = 0;
+
+ /**
+ * Gets the underlying value of this attribute.
+ *
+ * @return a SMILValue object. returned_object.IsNull() will be true if an
+ * error occurred.
+ */
+ virtual SMILValue 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 SMILValue& aValue) = 0;
+
+ /**
+ * Returns the targeted content node, for any SMILAttr implementations
+ * that want to expose that to the animation logic. Otherwise, returns
+ * null.
+ *
+ * @return the targeted content node, if this SMILAttr 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 ~SMILAttr() = default;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILATTR_H_
diff --git a/dom/smil/SMILBoolType.cpp b/dom/smil/SMILBoolType.cpp
new file mode 100644
index 0000000000..218b7af16f
--- /dev/null
+++ b/dom/smil/SMILBoolType.cpp
@@ -0,0 +1,69 @@
+/* -*- 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 "mozilla/SMILValue.h"
+#include "nsDebug.h"
+#include <math.h>
+
+namespace mozilla {
+
+void SMILBoolType::Init(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.IsNull(), "Unexpected value type");
+ aValue.mU.mBool = false;
+ aValue.mType = this;
+}
+
+void SMILBoolType::Destroy(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value");
+ aValue.mU.mBool = false;
+ aValue.mType = SMILNullType::Singleton();
+}
+
+nsresult SMILBoolType::Assign(SMILValue& aDest, const SMILValue& aSrc) const {
+ MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value");
+ aDest.mU.mBool = aSrc.mU.mBool;
+ return NS_OK;
+}
+
+bool SMILBoolType::IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const {
+ MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value");
+
+ return aLeft.mU.mBool == aRight.mU.mBool;
+}
+
+nsresult SMILBoolType::Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const {
+ MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types");
+ MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type");
+ return NS_ERROR_FAILURE; // bool values can't be added to each other
+}
+
+nsresult SMILBoolType::ComputeDistance(const SMILValue& aFrom,
+ const SMILValue& aTo,
+ double& aDistance) const {
+ MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types");
+ MOZ_ASSERT(aFrom.mType == this, "Unexpected source type");
+ return NS_ERROR_FAILURE; // there is no concept of distance between bool
+ // values
+}
+
+nsresult SMILBoolType::Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal,
+ double aUnitDistance,
+ SMILValue& 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");
+ 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 0000000000..472f1f9067
--- /dev/null
+++ b/dom/smil/SMILBoolType.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 DOM_SMIL_SMILBOOLTYPE_H_
+#define DOM_SMIL_SMILBOOLTYPE_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILType.h"
+
+namespace mozilla {
+
+class SMILBoolType : public SMILType {
+ public:
+ // Singleton for SMILValue objects to hold onto.
+ static SMILBoolType* Singleton() {
+ static SMILBoolType sSingleton;
+ return &sSingleton;
+ }
+
+ protected:
+ // SMILType Methods
+ // -------------------
+ virtual void Init(SMILValue& aValue) const override;
+ virtual void Destroy(SMILValue& aValue) const override;
+ virtual nsresult Assign(SMILValue& aDest,
+ const SMILValue& aSrc) const override;
+ virtual nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const override;
+ virtual bool IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const override;
+ virtual nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo,
+ double& aDistance) const override;
+ virtual nsresult Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal, double aUnitDistance,
+ SMILValue& aResult) const override;
+
+ private:
+ // Private constructor: prevent instances beyond my singleton.
+ constexpr SMILBoolType() = default;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILBOOLTYPE_H_
diff --git a/dom/smil/SMILCSSProperty.cpp b/dom/smil/SMILCSSProperty.cpp
new file mode 100644
index 0000000000..fb79a5f348
--- /dev/null
+++ b/dom/smil/SMILCSSProperty.cpp
@@ -0,0 +1,197 @@
+/* -*- 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 "SMILCSSProperty.h"
+
+#include <utility>
+
+#include "mozilla/SMILCSSValueType.h"
+#include "mozilla/SMILValue.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/dom/Element.h"
+#include "nsCSSProps.h"
+#include "nsDOMCSSAttrDeclaration.h"
+
+namespace mozilla {
+
+// Class Methods
+SMILCSSProperty::SMILCSSProperty(nsCSSPropertyID aPropID,
+ dom::Element* aElement,
+ ComputedStyle* aBaseComputedStyle)
+ : mPropID(aPropID),
+ mElement(aElement),
+ mBaseComputedStyle(aBaseComputedStyle) {
+ MOZ_ASSERT(IsPropertyAnimatable(mPropID),
+ "Creating a SMILCSSProperty for a property "
+ "that's not supported for animation");
+}
+
+SMILValue SMILCSSProperty::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:
+ SMILValue baseValue;
+
+ // SPECIAL CASE: (a) Shorthands
+ // (b) 'display'
+ // (c) No base ComputedStyle
+ if (nsCSSProps::IsShorthand(mPropID) || mPropID == eCSSProperty_display ||
+ !mBaseComputedStyle) {
+ // 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.
+ //
+ // Furthermore, if we don't (yet) have a base ComputedStyle we obviously
+ // can't resolve a base value.
+ //
+ // In any case, just return a dummy value (initialized with the right
+ // type, so as not to indicate failure).
+ SMILValue tmpVal(&SMILCSSValueType::sSingleton);
+ std::swap(baseValue, tmpVal);
+ return baseValue;
+ }
+
+ AnimationValue computedValue;
+ computedValue.mServo =
+ Servo_ComputedValues_ExtractAnimationValue(mBaseComputedStyle, mPropID)
+ .Consume();
+ if (!computedValue.mServo) {
+ return baseValue;
+ }
+
+ baseValue = SMILCSSValueType::ValueFromAnimationValue(mPropID, mElement,
+ computedValue);
+ return baseValue;
+}
+
+nsresult SMILCSSProperty::ValueFromString(
+ const nsAString& aStr, const dom::SVGAnimationElement* aSrcElement,
+ SMILValue& aValue, bool& aPreventCachingOfSandwich) const {
+ NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE);
+
+ SMILCSSValueType::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 SMILCSSProperty::SetAnimValue(const SMILValue& aValue) {
+ NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE);
+ return mElement->SMILOverrideStyle()->SetSMILValue(mPropID, aValue);
+}
+
+void SMILCSSProperty::ClearAnimValue() {
+ // Put empty string in override style for our property
+ mElement->SMILOverrideStyle()->SetPropertyValue(mPropID, ""_ns, nullptr,
+ IgnoreErrors());
+}
+
+// Based on http://www.w3.org/TR/SVG/propidx.html
+// static
+bool SMILCSSProperty::IsPropertyAnimatable(nsCSSPropertyID aPropID) {
+ // NOTE: Right now, Gecko doesn't recognize the following properties from
+ // the SVG Property Index:
+ // alignment-baseline
+ // baseline-shift
+ // color-profile
+ // 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;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILCSSProperty.h b/dom/smil/SMILCSSProperty.h
new file mode 100644
index 0000000000..f76ce9ed75
--- /dev/null
+++ b/dom/smil/SMILCSSProperty.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/. */
+
+/* representation of a SMIL-animatable CSS property on an element */
+
+#ifndef DOM_SMIL_SMILCSSPROPERTY_H_
+#define DOM_SMIL_SMILCSSPROPERTY_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILAttr.h"
+#include "nsAtom.h"
+#include "nsCSSPropertyID.h"
+#include "nsCSSValue.h"
+
+namespace mozilla {
+class ComputedStyle;
+namespace dom {
+class Element;
+} // namespace dom
+
+/**
+ * SMILCSSProperty: Implements the SMILAttr interface for SMIL animations
+ * that target CSS properties. Represents a particular animation-targeted CSS
+ * property on a particular element.
+ */
+class SMILCSSProperty : public SMILAttr {
+ public:
+ /**
+ * Constructs a new SMILCSSProperty.
+ * @param aPropID The CSS property we're interested in animating.
+ * @param aElement The element whose CSS property is being animated.
+ * @param aBaseComputedStyle The ComputedStyle to use when getting the base
+ * value. If this is nullptr and GetBaseValue is
+ * called, an empty SMILValue initialized with
+ * the SMILCSSValueType will be returned.
+ */
+ SMILCSSProperty(nsCSSPropertyID aPropID, dom::Element* aElement,
+ ComputedStyle* aBaseComputedStyle);
+
+ // SMILAttr methods
+ virtual nsresult ValueFromString(
+ const nsAString& aStr, const dom::SVGAnimationElement* aSrcElement,
+ SMILValue& aValue, bool& aPreventCachingOfSandwich) const override;
+ virtual SMILValue GetBaseValue() const override;
+ virtual nsresult SetAnimValue(const SMILValue& 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 SMILAttr (like me) only lives as long
+ // as the Compositing step, and DOM elements don't get a chance to die during
+ // that time.
+ dom::Element* mElement;
+
+ // The style to use when fetching base styles.
+ //
+ // As with mElement, since a SMILAttr only lives as long as the
+ // compositing step and since ComposeAttribute holds an owning reference to
+ // the base ComputedStyle, we can use a non-owning reference here.
+ ComputedStyle* mBaseComputedStyle;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILCSSPROPERTY_H_
diff --git a/dom/smil/SMILCSSValueType.cpp b/dom/smil/SMILCSSValueType.cpp
new file mode 100644
index 0000000000..be2048b4e9
--- /dev/null
+++ b/dom/smil/SMILCSSValueType.cpp
@@ -0,0 +1,573 @@
+/* -*- 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 "SMILCSSValueType.h"
+
+#include "nsComputedDOMStyle.h"
+#include "nsColor.h"
+#include "nsCSSProps.h"
+#include "nsCSSValue.h"
+#include "nsDebug.h"
+#include "nsPresContext.h"
+#include "nsString.h"
+#include "nsStyleUtil.h"
+#include "mozilla/DeclarationBlock.h"
+#include "mozilla/PresShell.h"
+#include "mozilla/PresShellInlines.h"
+#include "mozilla/ServoBindings.h"
+#include "mozilla/StyleAnimationValue.h"
+#include "mozilla/ServoCSSParser.h"
+#include "mozilla/ServoStyleSet.h"
+#include "mozilla/SMILParserUtils.h"
+#include "mozilla/SMILValue.h"
+#include "mozilla/dom/BaseKeyframeTypesBinding.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+using ServoAnimationValues =
+ CopyableAutoTArray<RefPtr<RawServoAnimationValue>, 1>;
+
+/*static*/
+SMILCSSValueType SMILCSSValueType::sSingleton;
+
+struct ValueWrapper {
+ ValueWrapper(nsCSSPropertyID aPropID, const AnimationValue& aValue)
+ : mPropID(aPropID) {
+ MOZ_ASSERT(!aValue.IsNull());
+ mServoValues.AppendElement(aValue.mServo);
+ }
+ ValueWrapper(nsCSSPropertyID aPropID,
+ const RefPtr<RawServoAnimationValue>& aValue)
+ : mPropID(aPropID), mServoValues{(aValue)} {}
+ ValueWrapper(nsCSSPropertyID aPropID, ServoAnimationValues&& aValues)
+ : mPropID(aPropID), mServoValues{std::move(aValues)} {}
+
+ bool operator==(const ValueWrapper& aOther) const {
+ if (mPropID != aOther.mPropID) {
+ return false;
+ }
+
+ MOZ_ASSERT(!mServoValues.IsEmpty());
+ size_t len = mServoValues.Length();
+ if (len != aOther.mServoValues.Length()) {
+ return false;
+ }
+ for (size_t i = 0; i < len; i++) {
+ if (!Servo_AnimationValue_DeepEqual(mServoValues[i],
+ aOther.mServoValues[i])) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ bool operator!=(const ValueWrapper& aOther) const {
+ return !(*this == aOther);
+ }
+
+ nsCSSPropertyID mPropID;
+ ServoAnimationValues mServoValues;
+};
+
+// Helper Methods
+// --------------
+
+// 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 simply returns true.
+//
+// If both arguments are null, this method returns false.
+//
+// |aZeroValueStorage| should be a reference to a
+// RefPtr<RawServoAnimationValue>. This is used where we may need to allocate a
+// new ServoAnimationValue to represent the appropriate zero value.
+//
+// Returns true on success, or otherwise.
+static bool FinalizeServoAnimationValues(
+ const RefPtr<RawServoAnimationValue>*& aValue1,
+ const RefPtr<RawServoAnimationValue>*& aValue2,
+ RefPtr<RawServoAnimationValue>& aZeroValueStorage) {
+ if (!aValue1 && !aValue2) {
+ return false;
+ }
+
+ // Are we missing either val? (If so, it's an implied 0 in other val's units)
+
+ if (!aValue1) {
+ aZeroValueStorage = Servo_AnimationValues_GetZeroValue(*aValue2).Consume();
+ aValue1 = &aZeroValueStorage;
+ } else if (!aValue2) {
+ aZeroValueStorage = Servo_AnimationValues_GetZeroValue(*aValue1).Consume();
+ aValue2 = &aZeroValueStorage;
+ }
+ return *aValue1 && *aValue2;
+}
+
+static ValueWrapper* ExtractValueWrapper(SMILValue& aValue) {
+ return static_cast<ValueWrapper*>(aValue.mU.mPtr);
+}
+
+static const ValueWrapper* ExtractValueWrapper(const SMILValue& aValue) {
+ return static_cast<const ValueWrapper*>(aValue.mU.mPtr);
+}
+
+// Class methods
+// -------------
+void SMILCSSValueType::Init(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.IsNull(), "Unexpected SMIL value type");
+
+ aValue.mU.mPtr = nullptr;
+ aValue.mType = this;
+}
+
+void SMILCSSValueType::Destroy(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value type");
+ delete static_cast<ValueWrapper*>(aValue.mU.mPtr);
+ aValue.mType = SMILNullType::Singleton();
+}
+
+nsresult SMILCSSValueType::Assign(SMILValue& aDest,
+ const SMILValue& 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 SMILCSSValueType::IsEqual(const SMILValue& aLeft,
+ const SMILValue& 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 SMILValues with matching ValueWrapper ptr");
+ return *leftWrapper == *rightWrapper;
+ }
+ // Left non-null, right null
+ return false;
+ }
+ if (rightWrapper) {
+ // Left null, right non-null
+ return false;
+ }
+ // Both null
+ return true;
+}
+
+static bool AddOrAccumulateForServo(SMILValue& aDest,
+ const ValueWrapper* aValueToAddWrapper,
+ ValueWrapper* aDestWrapper,
+ CompositeOperation aCompositeOp,
+ uint64_t aCount) {
+ nsCSSPropertyID property =
+ aValueToAddWrapper ? aValueToAddWrapper->mPropID : aDestWrapper->mPropID;
+ size_t len = aValueToAddWrapper ? aValueToAddWrapper->mServoValues.Length()
+ : aDestWrapper->mServoValues.Length();
+
+ MOZ_ASSERT(!aValueToAddWrapper || !aDestWrapper ||
+ aValueToAddWrapper->mServoValues.Length() ==
+ aDestWrapper->mServoValues.Length(),
+ "Both of values' length in the wrappers should be the same if "
+ "both of them exist");
+
+ for (size_t i = 0; i < len; i++) {
+ const RefPtr<RawServoAnimationValue>* valueToAdd =
+ aValueToAddWrapper ? &aValueToAddWrapper->mServoValues[i] : nullptr;
+ const RefPtr<RawServoAnimationValue>* destValue =
+ aDestWrapper ? &aDestWrapper->mServoValues[i] : nullptr;
+ RefPtr<RawServoAnimationValue> zeroValueStorage;
+ if (!FinalizeServoAnimationValues(valueToAdd, destValue,
+ zeroValueStorage)) {
+ return false;
+ }
+
+ // FinalizeServoAnimationValues may have updated destValue so we should make
+ // sure the aDest and aDestWrapper outparams are up-to-date.
+ if (aDestWrapper) {
+ aDestWrapper->mServoValues[i] = *destValue;
+ } else {
+ // aDest may be a barely-initialized "zero" destination.
+ aDest.mU.mPtr = aDestWrapper = new ValueWrapper(property, *destValue);
+ aDestWrapper->mServoValues.SetLength(len);
+ }
+
+ RefPtr<RawServoAnimationValue> result;
+ if (aCompositeOp == CompositeOperation::Add) {
+ result = Servo_AnimationValues_Add(*destValue, *valueToAdd).Consume();
+ } else {
+ result = Servo_AnimationValues_Accumulate(*destValue, *valueToAdd, aCount)
+ .Consume();
+ }
+
+ if (!result) {
+ return false;
+ }
+ aDestWrapper->mServoValues[i] = result;
+ }
+
+ return true;
+}
+
+static bool AddOrAccumulate(SMILValue& aDest, const SMILValue& aValueToAdd,
+ CompositeOperation aCompositeOp, uint64_t aCount) {
+ MOZ_ASSERT(aValueToAdd.mType == aDest.mType,
+ "Trying to add mismatching types");
+ MOZ_ASSERT(aValueToAdd.mType == &SMILCSSValueType::sSingleton,
+ "Unexpected SMIL value type");
+ MOZ_ASSERT(aCompositeOp == CompositeOperation::Add ||
+ aCompositeOp == CompositeOperation::Accumulate,
+ "Composite operation should be add or accumulate");
+ MOZ_ASSERT(aCompositeOp != CompositeOperation::Add || aCount == 1,
+ "Count should be 1 if composite operation is add");
+
+ ValueWrapper* destWrapper = ExtractValueWrapper(aDest);
+ const ValueWrapper* valueToAddWrapper = ExtractValueWrapper(aValueToAdd);
+
+ // If both of the values are empty just fail. This can happen in rare cases
+ // such as when the underlying animation produced an empty value.
+ //
+ // Technically, it doesn't matter what we return here since in either case it
+ // will produce the same result: an empty value.
+ if (!destWrapper && !valueToAddWrapper) {
+ return false;
+ }
+
+ 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 false;
+ }
+ // Skip font shorthand since it includes font-size-adjust.
+ if (property == eCSSProperty_font) {
+ return false;
+ }
+
+ return AddOrAccumulateForServo(aDest, valueToAddWrapper, destWrapper,
+ aCompositeOp, aCount);
+}
+
+nsresult SMILCSSValueType::SandwichAdd(SMILValue& aDest,
+ const SMILValue& aValueToAdd) const {
+ return AddOrAccumulate(aDest, aValueToAdd, CompositeOperation::Add, 1)
+ ? NS_OK
+ : NS_ERROR_FAILURE;
+}
+
+nsresult SMILCSSValueType::Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const {
+ return AddOrAccumulate(aDest, aValueToAdd, CompositeOperation::Accumulate,
+ aCount)
+ ? NS_OK
+ : NS_ERROR_FAILURE;
+}
+
+static nsresult ComputeDistanceForServo(const ValueWrapper* aFromWrapper,
+ const ValueWrapper& aToWrapper,
+ double& aDistance) {
+ size_t len = aToWrapper.mServoValues.Length();
+ MOZ_ASSERT(!aFromWrapper || aFromWrapper->mServoValues.Length() == len,
+ "From and to values length should be the same if "
+ "The start value exists");
+
+ double squareDistance = 0;
+
+ for (size_t i = 0; i < len; i++) {
+ const RefPtr<RawServoAnimationValue>* fromValue =
+ aFromWrapper ? &aFromWrapper->mServoValues[0] : nullptr;
+ const RefPtr<RawServoAnimationValue>* toValue = &aToWrapper.mServoValues[0];
+ RefPtr<RawServoAnimationValue> zeroValueStorage;
+ if (!FinalizeServoAnimationValues(fromValue, toValue, zeroValueStorage)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ double distance =
+ Servo_AnimationValues_ComputeDistance(*fromValue, *toValue);
+ if (distance < 0.0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (len == 1) {
+ aDistance = distance;
+ return NS_OK;
+ }
+ squareDistance += distance * distance;
+ }
+
+ aDistance = sqrt(squareDistance);
+
+ return NS_OK;
+}
+
+nsresult SMILCSSValueType::ComputeDistance(const SMILValue& aFrom,
+ const SMILValue& 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");
+ return ComputeDistanceForServo(fromWrapper, *toWrapper, aDistance);
+}
+
+static nsresult InterpolateForServo(const ValueWrapper* aStartWrapper,
+ const ValueWrapper& aEndWrapper,
+ double aUnitDistance, SMILValue& aResult) {
+ // For discretely-animated properties Servo_AnimationValues_Interpolate will
+ // perform the discrete animation (i.e. 50% flip) and return a success result.
+ // However, SMIL has its own special discrete animation behavior that it uses
+ // when keyTimes are specified, but we won't run that unless that this method
+ // returns a failure to indicate that the property cannot be smoothly
+ // interpolated, i.e. that we need to use a discrete calcMode.
+ //
+ // For shorthands, Servo_Property_IsDiscreteAnimatable will always return
+ // false. That's fine since most shorthands (like 'font' and
+ // 'text-decoration') include non-discrete components. If authors want to
+ // treat all components as discrete then they should use calcMode="discrete".
+ if (Servo_Property_IsDiscreteAnimatable(aEndWrapper.mPropID)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ServoAnimationValues results;
+ size_t len = aEndWrapper.mServoValues.Length();
+ results.SetCapacity(len);
+ MOZ_ASSERT(!aStartWrapper || aStartWrapper->mServoValues.Length() == len,
+ "Start and end values length should be the same if "
+ "the start value exists");
+ for (size_t i = 0; i < len; i++) {
+ const RefPtr<RawServoAnimationValue>* startValue =
+ aStartWrapper ? &aStartWrapper->mServoValues[i] : nullptr;
+ const RefPtr<RawServoAnimationValue>* endValue =
+ &aEndWrapper.mServoValues[i];
+ RefPtr<RawServoAnimationValue> zeroValueStorage;
+ if (!FinalizeServoAnimationValues(startValue, endValue, zeroValueStorage)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<RawServoAnimationValue> result =
+ Servo_AnimationValues_Interpolate(*startValue, *endValue, aUnitDistance)
+ .Consume();
+ if (!result) {
+ return NS_ERROR_FAILURE;
+ }
+ results.AppendElement(result);
+ }
+ aResult.mU.mPtr = new ValueWrapper(aEndWrapper.mPropID, std::move(results));
+
+ return NS_OK;
+}
+
+nsresult SMILCSSValueType::Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal,
+ double aUnitDistance,
+ SMILValue& 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");
+ return InterpolateForServo(startWrapper, *endWrapper, aUnitDistance, aResult);
+}
+
+static ServoAnimationValues ValueFromStringHelper(nsCSSPropertyID aPropID,
+ Element* aTargetElement,
+ nsPresContext* aPresContext,
+ ComputedStyle* aComputedStyle,
+ const nsAString& aString) {
+ ServoAnimationValues result;
+
+ Document* doc = aTargetElement->GetComposedDoc();
+ if (!doc) {
+ return result;
+ }
+
+ // Parse property
+ ServoCSSParser::ParsingEnvironment env =
+ ServoCSSParser::GetParsingEnvironment(doc);
+ RefPtr<RawServoDeclarationBlock> servoDeclarationBlock =
+ ServoCSSParser::ParseProperty(aPropID, NS_ConvertUTF16toUTF8(aString),
+ env,
+ ParsingMode::AllowUnitlessLength |
+ ParsingMode::AllowAllNumericValues);
+ if (!servoDeclarationBlock) {
+ return result;
+ }
+
+ // Compute value
+ aPresContext->StyleSet()->GetAnimationValues(
+ servoDeclarationBlock, aTargetElement, aComputedStyle, result);
+
+ return result;
+}
+
+// static
+void SMILCSSValueType::ValueFromString(nsCSSPropertyID aPropID,
+ Element* aTargetElement,
+ const nsAString& aString,
+ SMILValue& aValue,
+ bool* aIsContextSensitive) {
+ MOZ_ASSERT(aValue.IsNull(), "Outparam should be null-typed");
+ nsPresContext* presContext =
+ nsContentUtils::GetContextForContent(aTargetElement);
+ if (!presContext) {
+ NS_WARNING("Not parsing animation value; unable to get PresContext");
+ return;
+ }
+
+ Document* doc = aTargetElement->GetComposedDoc();
+ if (doc && !nsStyleUtil::CSPAllowsInlineStyle(nullptr, doc, nullptr, 0, 0,
+ aString, nullptr)) {
+ return;
+ }
+
+ RefPtr<ComputedStyle> computedStyle =
+ nsComputedDOMStyle::GetComputedStyle(aTargetElement, nullptr);
+ if (!computedStyle) {
+ return;
+ }
+
+ ServoAnimationValues parsedValues = ValueFromStringHelper(
+ aPropID, aTargetElement, presContext, computedStyle, aString);
+ if (aIsContextSensitive) {
+ // FIXME: Bug 1358955 - detect context-sensitive values and set this value
+ // appropriately.
+ *aIsContextSensitive = false;
+ }
+
+ if (!parsedValues.IsEmpty()) {
+ sSingleton.Init(aValue);
+ aValue.mU.mPtr = new ValueWrapper(aPropID, std::move(parsedValues));
+ }
+}
+
+// static
+SMILValue SMILCSSValueType::ValueFromAnimationValue(
+ nsCSSPropertyID aPropID, Element* aTargetElement,
+ const AnimationValue& aValue) {
+ SMILValue result;
+
+ Document* doc = aTargetElement->GetComposedDoc();
+ // We'd like to avoid serializing |aValue| if possible, and since the
+ // string passed to CSPAllowsInlineStyle is only used for reporting violations
+ // and an intermediate CSS value is not likely to be particularly useful
+ // in that case, we just use a generic placeholder string instead.
+ static const nsLiteralString kPlaceholderText = u"[SVG animation of CSS]"_ns;
+ if (doc && !nsStyleUtil::CSPAllowsInlineStyle(nullptr, doc, nullptr, 0, 0,
+ kPlaceholderText, nullptr)) {
+ return result;
+ }
+
+ sSingleton.Init(result);
+ result.mU.mPtr = new ValueWrapper(aPropID, aValue);
+
+ return result;
+}
+
+// static
+bool SMILCSSValueType::SetPropertyValues(const SMILValue& aValue,
+ DeclarationBlock& aDecl) {
+ MOZ_ASSERT(aValue.mType == &SMILCSSValueType::sSingleton,
+ "Unexpected SMIL value type");
+ const ValueWrapper* wrapper = ExtractValueWrapper(aValue);
+ if (!wrapper) {
+ return false;
+ }
+
+ bool changed = false;
+ for (const auto& value : wrapper->mServoValues) {
+ changed |= Servo_DeclarationBlock_SetPropertyToAnimationValue(aDecl.Raw(),
+ value, {});
+ }
+
+ return changed;
+}
+
+// static
+nsCSSPropertyID SMILCSSValueType::PropertyFromValue(const SMILValue& aValue) {
+ if (aValue.mType != &SMILCSSValueType::sSingleton) {
+ return eCSSProperty_UNKNOWN;
+ }
+
+ const ValueWrapper* wrapper = ExtractValueWrapper(aValue);
+ if (!wrapper) {
+ return eCSSProperty_UNKNOWN;
+ }
+
+ return wrapper->mPropID;
+}
+
+// static
+void SMILCSSValueType::FinalizeValue(SMILValue& aValue,
+ const SMILValue& aValueToMatch) {
+ MOZ_ASSERT(aValue.mType == aValueToMatch.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aValue.mType == &SMILCSSValueType::sSingleton,
+ "Unexpected SMIL value type");
+
+ ValueWrapper* valueWrapper = ExtractValueWrapper(aValue);
+ // If |aValue| already has a value, there's nothing to do here.
+ if (valueWrapper) {
+ return;
+ }
+
+ const ValueWrapper* valueToMatchWrapper = ExtractValueWrapper(aValueToMatch);
+ if (!valueToMatchWrapper) {
+ MOZ_ASSERT_UNREACHABLE("Value to match is empty");
+ return;
+ }
+
+ ServoAnimationValues zeroValues;
+ zeroValues.SetCapacity(valueToMatchWrapper->mServoValues.Length());
+
+ for (auto& valueToMatch : valueToMatchWrapper->mServoValues) {
+ RefPtr<RawServoAnimationValue> zeroValue =
+ Servo_AnimationValues_GetZeroValue(valueToMatch).Consume();
+ if (!zeroValue) {
+ return;
+ }
+ zeroValues.AppendElement(std::move(zeroValue));
+ }
+ aValue.mU.mPtr =
+ new ValueWrapper(valueToMatchWrapper->mPropID, std::move(zeroValues));
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILCSSValueType.h b/dom/smil/SMILCSSValueType.h
new file mode 100644
index 0000000000..72c66bdc43
--- /dev/null
+++ b/dom/smil/SMILCSSValueType.h
@@ -0,0 +1,133 @@
+/* -*- 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 DOM_SMIL_SMILCSSVALUETYPE_H_
+#define DOM_SMIL_SMILCSSVALUETYPE_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILType.h"
+#include "nsCSSPropertyID.h"
+#include "nsStringFwd.h"
+
+namespace mozilla {
+struct AnimationValue;
+class DeclarationBlock;
+namespace dom {
+class Element;
+} // namespace dom
+
+/*
+ * SMILCSSValueType: Represents a SMIL-animated CSS value.
+ */
+class SMILCSSValueType : public SMILType {
+ public:
+ // Singleton for SMILValue objects to hold onto.
+ static SMILCSSValueType sSingleton;
+
+ protected:
+ // SMILType Methods
+ // -------------------
+ void Init(SMILValue& aValue) const override;
+ void Destroy(SMILValue&) const override;
+ nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override;
+ bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override;
+ nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const override;
+ nsresult SandwichAdd(SMILValue& aDest,
+ const SMILValue& aValueToAdd) const override;
+ nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo,
+ double& aDistance) const override;
+ nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal,
+ double aUnitDistance, SMILValue& aResult) const override;
+
+ public:
+ // Helper Methods
+ // --------------
+ /**
+ * Sets up the given SMILValue 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 == SMILNullType::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 SMILValue 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 == SMILCSSValueType::sSingleton
+ */
+ static void ValueFromString(nsCSSPropertyID aPropID,
+ dom::Element* aTargetElement,
+ const nsAString& aString, SMILValue& aValue,
+ bool* aIsContextSensitive);
+
+ /**
+ * Creates a SMILValue to wrap the given animation value.
+ *
+ * @param aPropID The property that |aValue| corresponds to.
+ * @param aTargetElement The target element to which the animation value
+ * applies.
+ * @param aValue The animation value to use.
+ * @return A new SMILValue. On failure, returns a
+ * SMILValue with the null type (i.e. rv.IsNull()
+ * returns true).
+ */
+ static SMILValue ValueFromAnimationValue(nsCSSPropertyID aPropID,
+ dom::Element* aTargetElement,
+ const AnimationValue& aValue);
+
+ /**
+ * Sets the relevant property values in the declaration block.
+ *
+ * Returns whether the declaration changed.
+ */
+ static bool SetPropertyValues(const SMILValue&, mozilla::DeclarationBlock&);
+
+ /**
+ * Return the CSS property animated by the specified value.
+ *
+ * @param aValue The SMILValue to examine.
+ * @return The nsCSSPropertyID enum value of the property animated
+ * by |aValue|, or eCSSProperty_UNKNOWN if the type of
+ * |aValue| is not SMILCSSValueType.
+ */
+ static nsCSSPropertyID PropertyFromValue(const SMILValue& aValue);
+
+ /**
+ * If |aValue| is an empty value, converts it to a suitable zero value by
+ * matching the type of value stored in |aValueToMatch|.
+ *
+ * There is no indication if this method fails. If a suitable zero value could
+ * not be created, |aValue| is simply unmodified.
+ *
+ * @param aValue The SMILValue (of type SMILCSSValueType) to
+ * possibly update.
+ * @param aValueToMatch A SMILValue (of type SMILCSSValueType) for which
+ * a corresponding zero value will be created if |aValue|
+ * is empty.
+ */
+ static void FinalizeValue(SMILValue& aValue, const SMILValue& aValueToMatch);
+
+ private:
+ // Private constructor: prevent instances beyond my singleton.
+ constexpr SMILCSSValueType() = default;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILCSSVALUETYPE_H_
diff --git a/dom/smil/SMILCompositor.cpp b/dom/smil/SMILCompositor.cpp
new file mode 100644
index 0000000000..45a51e8b81
--- /dev/null
+++ b/dom/smil/SMILCompositor.cpp
@@ -0,0 +1,239 @@
+/* -*- 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 "SMILCompositor.h"
+
+#include "mozilla/dom/SVGSVGElement.h"
+#include "nsComputedDOMStyle.h"
+#include "nsCSSProps.h"
+#include "nsHashKeys.h"
+#include "SMILCSSProperty.h"
+
+namespace mozilla {
+
+// PLDHashEntryHdr methods
+bool SMILCompositor::KeyEquals(KeyTypePointer aKey) const {
+ return aKey && aKey->Equals(mKey);
+}
+
+/*static*/
+PLDHashNumber SMILCompositor::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());
+}
+
+// Cycle-collection support
+void SMILCompositor::Traverse(nsCycleCollectionTraversalCallback* aCallback) {
+ if (!mKey.mElement) return;
+
+ NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCallback, "Compositor mKey.mElement");
+ aCallback->NoteXPCOMChild(mKey.mElement);
+}
+
+// Other methods
+void SMILCompositor::AddAnimationFunction(SMILAnimationFunction* aFunc) {
+ if (aFunc) {
+ mAnimationFunctions.AppendElement(aFunc);
+ }
+}
+
+void SMILCompositor::ComposeAttribute(bool& aMightHavePendingStyleUpdates) {
+ if (!mKey.mElement) return;
+
+ // If we might need to resolve base styles, grab a suitable ComputedStyle
+ // for initializing our SMILAttr with.
+ RefPtr<ComputedStyle> baseComputedStyle;
+ if (MightNeedBaseStyle()) {
+ baseComputedStyle = nsComputedDOMStyle::GetUnanimatedComputedStyleNoFlush(
+ mKey.mElement, nullptr);
+ }
+
+ // FIRST: Get the SMILAttr (to grab base value from, and to eventually
+ // give animated value to)
+ UniquePtr<SMILAttr> smilAttr = CreateSMILAttr(baseComputedStyle);
+ if (!smilAttr) {
+ // Target attribute not found (or, out of memory)
+ return;
+ }
+ if (mAnimationFunctions.IsEmpty()) {
+ // No active animation functions. (We can still have a SMILCompositor 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.
+ SMILAnimationFunction::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
+ SMILValue 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("SMILAttr::SetAnimValue failed");
+ }
+}
+
+void SMILCompositor::ClearAnimationEffects() {
+ if (!mKey.mElement || !mKey.mAttributeName) return;
+
+ UniquePtr<SMILAttr> smilAttr = CreateSMILAttr(nullptr);
+ if (!smilAttr) {
+ // Target attribute not found (or, out of memory)
+ return;
+ }
+ smilAttr->ClearAnimValue();
+}
+
+// Protected Helper Functions
+// --------------------------
+UniquePtr<SMILAttr> SMILCompositor::CreateSMILAttr(
+ ComputedStyle* aBaseComputedStyle) {
+ nsCSSPropertyID propID = GetCSSPropertyToAnimate();
+
+ if (propID != eCSSProperty_UNKNOWN) {
+ return MakeUnique<SMILCSSProperty>(propID, mKey.mElement.get(),
+ aBaseComputedStyle);
+ }
+
+ return mKey.mElement->GetAnimatedAttr(mKey.mAttributeNamespaceID,
+ mKey.mAttributeName);
+}
+
+nsCSSPropertyID SMILCompositor::GetCSSPropertyToAnimate() const {
+ if (mKey.mAttributeNamespaceID != kNameSpaceID_None) {
+ return eCSSProperty_UNKNOWN;
+ }
+
+ nsCSSPropertyID propID =
+ nsCSSProps::LookupProperty(nsAtomCString(mKey.mAttributeName));
+
+ if (!SMILCSSProperty::IsPropertyAnimatable(propID)) {
+ return eCSSProperty_UNKNOWN;
+ }
+
+ // If we are animating the 'width' or 'height' of an outer SVG
+ // element we should animate it as a CSS property, but for other elements
+ // in SVG namespace (e.g. <rect>) we should animate it as a length attribute.
+ if ((mKey.mAttributeName == nsGkAtoms::width ||
+ mKey.mAttributeName == nsGkAtoms::height) &&
+ mKey.mElement->GetNameSpaceID() == kNameSpaceID_SVG) {
+ // Not an <svg> element.
+ if (!mKey.mElement->IsSVGElement(nsGkAtoms::svg)) {
+ return eCSSProperty_UNKNOWN;
+ }
+
+ // An inner <svg> element
+ if (static_cast<dom::SVGSVGElement const&>(*mKey.mElement).IsInner()) {
+ return eCSSProperty_UNKNOWN;
+ }
+
+ // Indeed an outer <svg> element, fall through.
+ }
+
+ return propID;
+}
+
+bool SMILCompositor::MightNeedBaseStyle() const {
+ if (GetCSSPropertyToAnimate() == eCSSProperty_UNKNOWN) {
+ return false;
+ }
+
+ // We should return true if at least one animation function might build on
+ // the base value.
+ for (const SMILAnimationFunction* func : mAnimationFunctions) {
+ if (!func->WillReplace()) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+uint32_t SMILCompositor::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) {
+ SMILAnimationFunction* 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 SMILCompositor::UpdateCachedBaseValue(const SMILValue& aBaseValue) {
+ if (mCachedBaseValue != aBaseValue) {
+ // Base value has changed since last sample.
+ mCachedBaseValue = aBaseValue;
+ mForceCompositing = true;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILCompositor.h b/dom/smil/SMILCompositor.h
new file mode 100644
index 0000000000..7e54ba5961
--- /dev/null
+++ b/dom/smil/SMILCompositor.h
@@ -0,0 +1,128 @@
+/* -*- 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 DOM_SMIL_SMILCOMPOSITOR_H_
+#define DOM_SMIL_SMILCOMPOSITOR_H_
+
+#include <utility>
+
+#include "PLDHashTable.h"
+#include "SMILTargetIdentifier.h"
+#include "mozilla/SMILAnimationFunction.h"
+#include "mozilla/SMILCompositorTable.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCSSPropertyID.h"
+#include "nsString.h"
+#include "nsTHashtable.h"
+
+namespace mozilla {
+
+class ComputedStyle;
+
+//----------------------------------------------------------------------
+// SMILCompositor
+//
+// 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 SMILCompositor : public PLDHashEntryHdr {
+ public:
+ using KeyType = SMILTargetIdentifier;
+ using KeyTypeRef = const KeyType&;
+ using KeyTypePointer = const KeyType*;
+
+ explicit SMILCompositor(KeyTypePointer aKey)
+ : mKey(*aKey), mForceCompositing(false) {}
+ SMILCompositor(SMILCompositor&& toMove) noexcept
+ : PLDHashEntryHdr(std::move(toMove)),
+ mKey(std::move(toMove.mKey)),
+ mAnimationFunctions(std::move(toMove.mAnimationFunctions)),
+ mForceCompositing(false) {}
+
+ // 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(SMILAnimationFunction* 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(SMILCompositor* aOther) {
+ mCachedBaseValue = std::move(aOther->mCachedBaseValue);
+ }
+
+ private:
+ // Create a SMILAttr for my target, on the heap.
+ //
+ // @param aBaseComputedStyle An optional ComputedStyle which, if set, will be
+ // used when fetching the base style.
+ UniquePtr<SMILAttr> CreateSMILAttr(ComputedStyle* aBaseComputedStyle);
+
+ // Returns the CSS property this compositor should animate, or
+ // eCSSProperty_UNKNOWN if this compositor does not animate a CSS property.
+ nsCSSPropertyID GetCSSPropertyToAnimate() const;
+
+ // Returns true if we might need to refer to base styles (i.e. we are
+ // targeting a CSS property and have one or more animation functions that
+ // don't just replace the underlying value).
+ //
+ // This might return true in some cases where we don't actually need the base
+ // style since it doesn't build up the animation sandwich to check if the
+ // functions that appear to need the base style are actually replaced by
+ // a function further up the stack.
+ bool MightNeedBaseStyle() const;
+
+ // 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 SMILValue& aBaseValue);
+
+ // The hash key (tuple of element and attributeName)
+ KeyType mKey;
+
+ // Hash Value: List of animation functions that animate the specified attr
+ nsTArray<SMILAnimationFunction*> 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. (SMILAnimationController moves this
+ // forward from the previous sample's compositor by calling
+ // StealCachedBaseValue.)
+ SMILValue mCachedBaseValue;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILCOMPOSITOR_H_
diff --git a/dom/smil/SMILCompositorTable.h b/dom/smil/SMILCompositorTable.h
new file mode 100644
index 0000000000..649c108872
--- /dev/null
+++ b/dom/smil/SMILCompositorTable.h
@@ -0,0 +1,28 @@
+/* -*- 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 DOM_SMIL_SMILCOMPOSITORTABLE_H_
+#define DOM_SMIL_SMILCOMPOSITORTABLE_H_
+
+#include "nsTHashtable.h"
+
+//----------------------------------------------------------------------
+// SMILCompositorTable : A hashmap of SMILCompositors
+//
+// This is just a forward-declaration because it is included in
+// SMILAnimationController which is used in Document. We don't want to
+// expose all of SMILCompositor or otherwise any changes to it will mean the
+// whole world will need to be rebuilt.
+
+namespace mozilla {
+
+class SMILCompositor;
+
+using SMILCompositorTable = nsTHashtable<SMILCompositor>;
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILCOMPOSITORTABLE_H_
diff --git a/dom/smil/SMILEnumType.cpp b/dom/smil/SMILEnumType.cpp
new file mode 100644
index 0000000000..fbc5c6d2b8
--- /dev/null
+++ b/dom/smil/SMILEnumType.cpp
@@ -0,0 +1,69 @@
+/* -*- 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 "mozilla/SMILValue.h"
+#include "nsDebug.h"
+#include <math.h>
+
+namespace mozilla {
+
+void SMILEnumType::Init(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.IsNull(), "Unexpected value type");
+ aValue.mU.mUint = 0;
+ aValue.mType = this;
+}
+
+void SMILEnumType::Destroy(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value");
+ aValue.mU.mUint = 0;
+ aValue.mType = SMILNullType::Singleton();
+}
+
+nsresult SMILEnumType::Assign(SMILValue& aDest, const SMILValue& aSrc) const {
+ MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value");
+ aDest.mU.mUint = aSrc.mU.mUint;
+ return NS_OK;
+}
+
+bool SMILEnumType::IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const {
+ MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value");
+
+ return aLeft.mU.mUint == aRight.mU.mUint;
+}
+
+nsresult SMILEnumType::Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const {
+ MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types");
+ MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type");
+ return NS_ERROR_FAILURE; // enum values can't be added to each other
+}
+
+nsresult SMILEnumType::ComputeDistance(const SMILValue& aFrom,
+ const SMILValue& aTo,
+ double& aDistance) const {
+ MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types");
+ MOZ_ASSERT(aFrom.mType == this, "Unexpected source type");
+ return NS_ERROR_FAILURE; // there is no concept of distance between enum
+ // values
+}
+
+nsresult SMILEnumType::Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal,
+ double aUnitDistance,
+ SMILValue& 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");
+ 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 0000000000..039d32f0bb
--- /dev/null
+++ b/dom/smil/SMILEnumType.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 DOM_SMIL_SMILENUMTYPE_H_
+#define DOM_SMIL_SMILENUMTYPE_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILType.h"
+
+namespace mozilla {
+
+class SMILEnumType : public SMILType {
+ public:
+ // Singleton for SMILValue objects to hold onto.
+ static SMILEnumType* Singleton() {
+ static SMILEnumType sSingleton;
+ return &sSingleton;
+ }
+
+ protected:
+ // SMILType Methods
+ // -------------------
+ virtual void Init(SMILValue& aValue) const override;
+ virtual void Destroy(SMILValue& aValue) const override;
+ virtual nsresult Assign(SMILValue& aDest,
+ const SMILValue& aSrc) const override;
+ virtual bool IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const override;
+ virtual nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const override;
+ virtual nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo,
+ double& aDistance) const override;
+ virtual nsresult Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal, double aUnitDistance,
+ SMILValue& aResult) const override;
+
+ private:
+ // Private constructor: prevent instances beyond my singleton.
+ constexpr SMILEnumType() = default;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILENUMTYPE_H_
diff --git a/dom/smil/SMILFloatType.cpp b/dom/smil/SMILFloatType.cpp
new file mode 100644
index 0000000000..ac50cfe8d2
--- /dev/null
+++ b/dom/smil/SMILFloatType.cpp
@@ -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/. */
+
+#include "SMILFloatType.h"
+
+#include "mozilla/SMILValue.h"
+#include "nsDebug.h"
+#include <math.h>
+
+namespace mozilla {
+
+void SMILFloatType::Init(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.IsNull(), "Unexpected value type");
+ aValue.mU.mDouble = 0.0;
+ aValue.mType = this;
+}
+
+void SMILFloatType::Destroy(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value");
+ aValue.mU.mDouble = 0.0;
+ aValue.mType = SMILNullType::Singleton();
+}
+
+nsresult SMILFloatType::Assign(SMILValue& aDest, const SMILValue& aSrc) const {
+ MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value");
+ aDest.mU.mDouble = aSrc.mU.mDouble;
+ return NS_OK;
+}
+
+bool SMILFloatType::IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const {
+ MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value");
+
+ return aLeft.mU.mDouble == aRight.mU.mDouble;
+}
+
+nsresult SMILFloatType::Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const {
+ MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types");
+ MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type");
+ aDest.mU.mDouble += aValueToAdd.mU.mDouble * aCount;
+ return NS_OK;
+}
+
+nsresult SMILFloatType::ComputeDistance(const SMILValue& aFrom,
+ const SMILValue& aTo,
+ double& aDistance) const {
+ MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types");
+ MOZ_ASSERT(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 SMILFloatType::Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal,
+ double aUnitDistance,
+ SMILValue& 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");
+
+ const double& startVal = aStartVal.mU.mDouble;
+ const double& endVal = aEndVal.mU.mDouble;
+
+ aResult.mU.mDouble = (startVal + (endVal - startVal) * aUnitDistance);
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILFloatType.h b/dom/smil/SMILFloatType.h
new file mode 100644
index 0000000000..9139fa621a
--- /dev/null
+++ b/dom/smil/SMILFloatType.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 DOM_SMIL_SMILFLOATTYPE_H_
+#define DOM_SMIL_SMILFLOATTYPE_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILType.h"
+
+namespace mozilla {
+
+class SMILFloatType : public SMILType {
+ public:
+ // Singleton for SMILValue objects to hold onto.
+ static SMILFloatType* Singleton() {
+ static SMILFloatType sSingleton;
+ return &sSingleton;
+ }
+
+ protected:
+ // SMILType Methods
+ // -------------------
+ virtual void Init(SMILValue& aValue) const override;
+ virtual void Destroy(SMILValue& aValue) const override;
+ virtual nsresult Assign(SMILValue& aDest,
+ const SMILValue& aSrc) const override;
+ virtual bool IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const override;
+ virtual nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const override;
+ virtual nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo,
+ double& aDistance) const override;
+ virtual nsresult Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal, double aUnitDistance,
+ SMILValue& aResult) const override;
+
+ private:
+ // Private constructor: prevent instances beyond my singleton.
+ constexpr SMILFloatType() = default;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILFLOATTYPE_H_
diff --git a/dom/smil/SMILInstanceTime.cpp b/dom/smil/SMILInstanceTime.cpp
new file mode 100644
index 0000000000..7f38b8c1d7
--- /dev/null
+++ b/dom/smil/SMILInstanceTime.cpp
@@ -0,0 +1,188 @@
+/* -*- 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 "SMILInstanceTime.h"
+
+#include "mozilla/AutoRestore.h"
+#include "mozilla/SMILInterval.h"
+#include "mozilla/SMILTimeValueSpec.h"
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// Implementation
+
+SMILInstanceTime::SMILInstanceTime(const SMILTimeValue& aTime,
+ SMILInstanceTimeSource aSource,
+ SMILTimeValueSpec* aCreator,
+ SMILInterval* 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);
+}
+
+SMILInstanceTime::~SMILInstanceTime() {
+ 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 SMILInstanceTime::Unlink() {
+ RefPtr<SMILInstanceTime> deathGrip(this);
+ if (mBaseInterval) {
+ mBaseInterval->RemoveDependentTime(*this);
+ mBaseInterval = nullptr;
+ }
+ mCreator = nullptr;
+}
+
+void SMILInstanceTime::HandleChangedInterval(
+ const SMILTimeContainer* 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<SMILInstanceTime> deathGrip(this);
+ mozilla::AutoRestore<bool> setVisited(mVisited);
+ mVisited = true;
+
+ mCreator->HandleChangedInstanceTime(*GetBaseTime(), aSrcContainer, *this,
+ objectChanged);
+}
+
+void SMILInstanceTime::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<SMILInstanceTime> deathGrip(this);
+ mCreator->HandleDeletedInstanceTime(*this);
+ mCreator = nullptr;
+}
+
+void SMILInstanceTime::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 SMILInstanceTime::ShouldPreserve() const {
+ return mFixedEndpointRefCnt > 0 || (mFlags & kWasDynamicEndpoint);
+}
+
+void SMILInstanceTime::UnmarkShouldPreserve() {
+ mFlags &= ~kWasDynamicEndpoint;
+}
+
+void SMILInstanceTime::AddRefFixedEndpoint() {
+ MOZ_ASSERT(mFixedEndpointRefCnt < UINT16_MAX,
+ "Fixed endpoint reference count upper limit reached");
+ ++mFixedEndpointRefCnt;
+ mFlags &= ~kMayUpdate; // Once fixed, always fixed
+}
+
+void SMILInstanceTime::ReleaseFixedEndpoint() {
+ MOZ_ASSERT(mFixedEndpointRefCnt > 0, "Duplicate release");
+ --mFixedEndpointRefCnt;
+ if (mFixedEndpointRefCnt == 0 && IsDynamic()) {
+ mFlags |= kWasDynamicEndpoint;
+ }
+}
+
+bool SMILInstanceTime::IsDependentOn(const SMILInstanceTime& aOther) const {
+ if (mVisited) return false;
+
+ const SMILInstanceTime* myBaseTime = GetBaseTime();
+ if (!myBaseTime) return false;
+
+ if (myBaseTime == &aOther) return true;
+
+ mozilla::AutoRestore<bool> setVisited(mVisited);
+ mVisited = true;
+ return myBaseTime->IsDependentOn(aOther);
+}
+
+const SMILInstanceTime* SMILInstanceTime::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 SMILInstanceTime::SetBaseInterval(SMILInterval* 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 SMILTimeValueSpec object.");
+ if (!mCreator) return;
+
+ aBaseInterval->AddDependentTime(*this);
+ }
+
+ mBaseInterval = aBaseInterval;
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILInstanceTime.h b/dom/smil/SMILInstanceTime.h
new file mode 100644
index 0000000000..1224cfc8fe
--- /dev/null
+++ b/dom/smil/SMILInstanceTime.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 DOM_SMIL_SMILINSTANCETIME_H_
+#define DOM_SMIL_SMILINSTANCETIME_H_
+
+#include "nsISupportsImpl.h"
+#include "mozilla/SMILTimeValue.h"
+
+namespace mozilla {
+class SMILInterval;
+class SMILTimeContainer;
+class SMILTimeValueSpec;
+
+//----------------------------------------------------------------------
+// SMILInstanceTime
+//
+// 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 SMILTimeValue.h
+//
+// These objects are owned by an SMILTimedElement but MAY also be referenced
+// by:
+//
+// a) SMILIntervals that belong to the same SMILTimedElement and which refer
+// to the SMILInstanceTimes which form the interval endpoints; and/or
+// b) SMILIntervals that belong to other SMILTimedElements 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
+// SMILInstanceTimes if its begin time changes. This notification is
+// performed by the SMILInterval.
+
+class SMILInstanceTime 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 SMILInstanceTimeSource {
+ // 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 SMILInstanceTime(const SMILTimeValue& aTime,
+ SMILInstanceTimeSource aSource = SOURCE_NONE,
+ SMILTimeValueSpec* aCreator = nullptr,
+ SMILInterval* aBaseInterval = nullptr);
+
+ void Unlink();
+ void HandleChangedInterval(const SMILTimeContainer* aSrcContainer,
+ bool aBeginObjectChanged, bool aEndObjectChanged);
+ void HandleDeletedInterval();
+ void HandleFilteredInterval();
+
+ const SMILTimeValue& Time() const { return mTime; }
+ const SMILTimeValueSpec* 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 SMILTimeValue& 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 SMILInstanceTime& aOther) const;
+ const SMILInterval* GetBaseInterval() const { return mBaseInterval; }
+ const SMILInstanceTime* GetBaseTime() const;
+
+ bool SameTimeAndBase(const SMILInstanceTime& 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(SMILInstanceTime)
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~SMILInstanceTime();
+
+ void SetBaseInterval(SMILInterval* aBaseInterval);
+
+ SMILTimeValue 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
+ // SMILTimeValueSpec and as such may be updated. Such instance time should
+ // not be filtered out by the SMILTimedElement 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 SMILTimeValueSpec. 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 SMILTimeValueSpec), 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 SMILTimedElement 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.
+
+ SMILTimeValueSpec* mCreator; // The SMILTimeValueSpec object that created
+ // us. (currently only needed for syncbase
+ // instance times.)
+ SMILInterval* mBaseInterval; // Interval from which this time is derived
+ // (only used for syncbase instance times)
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILINSTANCETIME_H_
diff --git a/dom/smil/SMILIntegerType.cpp b/dom/smil/SMILIntegerType.cpp
new file mode 100644
index 0000000000..c1ac207836
--- /dev/null
+++ b/dom/smil/SMILIntegerType.cpp
@@ -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/. */
+
+#include "SMILIntegerType.h"
+
+#include "mozilla/SMILValue.h"
+#include "nsDebug.h"
+#include <math.h>
+
+namespace mozilla {
+
+void SMILIntegerType::Init(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.IsNull(), "Unexpected value type");
+ aValue.mU.mInt = 0;
+ aValue.mType = this;
+}
+
+void SMILIntegerType::Destroy(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value");
+ aValue.mU.mInt = 0;
+ aValue.mType = SMILNullType::Singleton();
+}
+
+nsresult SMILIntegerType::Assign(SMILValue& aDest,
+ const SMILValue& aSrc) const {
+ MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value");
+ aDest.mU.mInt = aSrc.mU.mInt;
+ return NS_OK;
+}
+
+bool SMILIntegerType::IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const {
+ MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value");
+
+ return aLeft.mU.mInt == aRight.mU.mInt;
+}
+
+nsresult SMILIntegerType::Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const {
+ MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types");
+ MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type");
+ aDest.mU.mInt += aValueToAdd.mU.mInt * aCount;
+ return NS_OK;
+}
+
+nsresult SMILIntegerType::ComputeDistance(const SMILValue& aFrom,
+ const SMILValue& aTo,
+ double& aDistance) const {
+ MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types");
+ MOZ_ASSERT(aFrom.mType == this, "Unexpected source type");
+ aDistance = fabs(double(aTo.mU.mInt - aFrom.mU.mInt));
+ return NS_OK;
+}
+
+nsresult SMILIntegerType::Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal,
+ double aUnitDistance,
+ SMILValue& 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");
+
+ 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 0000000000..5e50ae8be1
--- /dev/null
+++ b/dom/smil/SMILIntegerType.h
@@ -0,0 +1,42 @@
+/* -*- 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 DOM_SMIL_SMILINTEGERTYPE_H_
+#define DOM_SMIL_SMILINTEGERTYPE_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILType.h"
+
+namespace mozilla {
+
+class SMILIntegerType : public SMILType {
+ public:
+ virtual void Init(SMILValue& aValue) const override;
+ virtual void Destroy(SMILValue& aValue) const override;
+ virtual nsresult Assign(SMILValue& aDest,
+ const SMILValue& aSrc) const override;
+ virtual bool IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const override;
+ virtual nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const override;
+ virtual nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo,
+ double& aDistance) const override;
+ virtual nsresult Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal, double aUnitDistance,
+ SMILValue& aResult) const override;
+
+ static SMILIntegerType* Singleton() {
+ static SMILIntegerType sSingleton;
+ return &sSingleton;
+ }
+
+ private:
+ constexpr SMILIntegerType() = default;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILINTEGERTYPE_H_
diff --git a/dom/smil/SMILInterval.cpp b/dom/smil/SMILInterval.cpp
new file mode 100644
index 0000000000..e0ee99ac00
--- /dev/null
+++ b/dom/smil/SMILInterval.cpp
@@ -0,0 +1,137 @@
+/* -*- 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 "SMILInterval.h"
+
+#include "mozilla/DebugOnly.h"
+
+namespace mozilla {
+
+SMILInterval::SMILInterval() : mBeginFixed(false), mEndFixed(false) {}
+
+SMILInterval::SMILInterval(const SMILInterval& 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");
+}
+
+SMILInterval::~SMILInterval() {
+ MOZ_ASSERT(mDependentTimes.IsEmpty(),
+ "Destroying interval without disassociating dependent instance "
+ "times. Unlink was not called");
+}
+
+void SMILInterval::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;
+}
+
+SMILInstanceTime* SMILInterval::Begin() {
+ MOZ_ASSERT(mBegin && mEnd, "Requesting Begin() on un-initialized interval.");
+ return mBegin;
+}
+
+SMILInstanceTime* SMILInterval::End() {
+ MOZ_ASSERT(mBegin && mEnd, "Requesting End() on un-initialized interval.");
+ return mEnd;
+}
+
+void SMILInterval::SetBegin(SMILInstanceTime& 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 SMILInterval::SetEnd(SMILInstanceTime& 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 SMILInterval::FixBegin() {
+ MOZ_ASSERT(mBegin && mEnd, "Fixing begin point on un-initialized interval");
+ MOZ_ASSERT(!mBeginFixed, "Duplicate calls to FixBegin()");
+ mBeginFixed = true;
+ mBegin->AddRefFixedEndpoint();
+}
+
+void SMILInterval::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 SMILInterval::AddDependentTime(SMILInstanceTime& aTime) {
+ RefPtr<SMILInstanceTime>* inserted =
+ mDependentTimes.InsertElementSorted(&aTime);
+ if (!inserted) {
+ NS_WARNING("Insufficient memory to insert instance time.");
+ }
+}
+
+void SMILInterval::RemoveDependentTime(const SMILInstanceTime& aTime) {
+ DebugOnly<bool> found = mDependentTimes.RemoveElementSorted(&aTime);
+ MOZ_ASSERT(found, "Couldn't find instance time to delete.");
+}
+
+void SMILInterval::GetDependentTimes(InstanceTimeList& aTimes) {
+ aTimes = mDependentTimes.Clone();
+}
+
+bool SMILInterval::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);
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILInterval.h b/dom/smil/SMILInterval.h
new file mode 100644
index 0000000000..a569fce147
--- /dev/null
+++ b/dom/smil/SMILInterval.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 DOM_SMIL_SMILINTERVAL_H_
+#define DOM_SMIL_SMILINTERVAL_H_
+
+#include "mozilla/SMILInstanceTime.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// SMILInterval 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 SMILTimeValue.h
+
+class SMILInterval {
+ public:
+ SMILInterval();
+ SMILInterval(const SMILInterval& aOther);
+ ~SMILInterval();
+ void Unlink(bool aFiltered = false);
+
+ const SMILInstanceTime* Begin() const {
+ MOZ_ASSERT(mBegin && mEnd,
+ "Requesting Begin() on un-initialized instance time");
+ return mBegin;
+ }
+ SMILInstanceTime* Begin();
+
+ const SMILInstanceTime* End() const {
+ MOZ_ASSERT(mBegin && mEnd,
+ "Requesting End() on un-initialized instance time");
+ return mEnd;
+ }
+ SMILInstanceTime* End();
+
+ void SetBegin(SMILInstanceTime& aBegin);
+ void SetEnd(SMILInstanceTime& aEnd);
+ void Set(SMILInstanceTime& aBegin, SMILInstanceTime& aEnd) {
+ SetBegin(aBegin);
+ SetEnd(aEnd);
+ }
+
+ void FixBegin();
+ void FixEnd();
+
+ using InstanceTimeList = nsTArray<RefPtr<SMILInstanceTime>>;
+
+ void AddDependentTime(SMILInstanceTime& aTime);
+ void RemoveDependentTime(const SMILInstanceTime& aTime);
+ void GetDependentTimes(InstanceTimeList& aTimes);
+
+ // Cue for assessing if this interval can be filtered
+ bool IsDependencyChainLink() const;
+
+ private:
+ RefPtr<SMILInstanceTime> mBegin;
+ RefPtr<SMILInstanceTime> mEnd;
+
+ // SMILInstanceTimes 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. SMILInstanceTime::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 SMILInstanceTime object.
+ //
+ // However, if mBegin/EndFixed is true, then BOTH the SMILInstanceTime
+ // OBJECT returned for that end point and its TIME value will not change.
+ bool mBeginFixed;
+ bool mEndFixed;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILINTERVAL_H_
diff --git a/dom/smil/SMILKeySpline.cpp b/dom/smil/SMILKeySpline.cpp
new file mode 100644
index 0000000000..dd508e1bcb
--- /dev/null
+++ b/dom/smil/SMILKeySpline.cpp
@@ -0,0 +1,127 @@
+/* -*- 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 "SMILKeySpline.h"
+#include <stdint.h>
+#include <math.h>
+
+namespace mozilla {
+
+#define NEWTON_ITERATIONS 4
+#define NEWTON_MIN_SLOPE 0.02
+#define SUBDIVISION_PRECISION 0.0000001
+#define SUBDIVISION_MAX_ITERATIONS 10
+
+const double SMILKeySpline::kSampleStepSize =
+ 1.0 / double(kSplineTableSize - 1);
+
+void SMILKeySpline::Init(double aX1, double aY1, double aX2, double aY2) {
+ mX1 = aX1;
+ mY1 = aY1;
+ mX2 = aX2;
+ mY2 = aY2;
+
+ if (mX1 != mY1 || mX2 != mY2) CalcSampleValues();
+}
+
+double SMILKeySpline::GetSplineValue(double aX) const {
+ if (mX1 == mY1 && mX2 == mY2) return aX;
+
+ return CalcBezier(GetTForX(aX), mY1, mY2);
+}
+
+void SMILKeySpline::GetSplineDerivativeValues(double aX, double& aDX,
+ double& aDY) const {
+ double t = GetTForX(aX);
+ aDX = GetSlope(t, mX1, mX2);
+ aDY = GetSlope(t, mY1, mY2);
+}
+
+void SMILKeySpline::CalcSampleValues() {
+ for (uint32_t i = 0; i < kSplineTableSize; ++i) {
+ mSampleValues[i] = CalcBezier(double(i) * kSampleStepSize, mX1, mX2);
+ }
+}
+
+/*static*/
+double SMILKeySpline::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 SMILKeySpline::GetSlope(double aT, double aA1, double aA2) {
+ return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1);
+}
+
+double SMILKeySpline::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);
+ }
+ if (initialSlope == 0.0) {
+ return guessForT;
+ }
+ return BinarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize);
+}
+
+double SMILKeySpline::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 SMILKeySpline::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;
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILKeySpline.h b/dom/smil/SMILKeySpline.h
new file mode 100644
index 0000000000..9b004d272d
--- /dev/null
+++ b/dom/smil/SMILKeySpline.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 DOM_SMIL_SMILKEYSPLINE_H_
+#define DOM_SMIL_SMILKEYSPLINE_H_
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/PodOperations.h"
+
+namespace mozilla {
+
+/**
+ * Utility class to provide scaling defined in a keySplines element.
+ */
+class SMILKeySpline {
+ public:
+ SMILKeySpline() : mX1(0), mY1(0), mX2(0), mY2(0) {
+ /* 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
+ */
+ SMILKeySpline(double aX1, double aY1, double aX2, double aY2)
+ : mX1(0), mY1(0), mX2(0), mY2(0) {
+ 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 SMILKeySpline& aOther) const {
+ return mX1 == aOther.mX1 && mY1 == aOther.mY1 && mX2 == aOther.mX2 &&
+ mY2 == aOther.mY2;
+ }
+ bool operator!=(const SMILKeySpline& aOther) const {
+ return !(*this == aOther);
+ }
+ int32_t Compare(const SMILKeySpline& 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;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILKEYSPLINE_H_
diff --git a/dom/smil/SMILMilestone.h b/dom/smil/SMILMilestone.h
new file mode 100644
index 0000000000..8b9372428f
--- /dev/null
+++ b/dom/smil/SMILMilestone.h
@@ -0,0 +1,75 @@
+/* -*- 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 DOM_SMIL_SMILMILESTONE_H_
+#define DOM_SMIL_SMILMILESTONE_H_
+
+#include "mozilla/SMILTypes.h"
+
+namespace mozilla {
+
+/*
+ * A significant moment in an SMILTimedElement'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
+ * SMILTimedElement) 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 SMILMilestone {
+ public:
+ SMILMilestone(SMILTime aTime, bool aIsEnd) : mTime(aTime), mIsEnd(aIsEnd) {}
+
+ SMILMilestone() : mTime(0), mIsEnd(false) {}
+
+ bool operator==(const SMILMilestone& aOther) const {
+ return mTime == aOther.mTime && mIsEnd == aOther.mIsEnd;
+ }
+
+ bool operator!=(const SMILMilestone& aOther) const {
+ return !(*this == aOther);
+ }
+
+ bool operator<(const SMILMilestone& 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 SMILMilestone& aOther) const {
+ return *this == aOther || *this < aOther;
+ }
+
+ bool operator>=(const SMILMilestone& aOther) const {
+ return !(*this < aOther);
+ }
+
+ SMILTime 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.
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILMILESTONE_H_
diff --git a/dom/smil/SMILNullType.cpp b/dom/smil/SMILNullType.cpp
new file mode 100644
index 0000000000..571f6f3afe
--- /dev/null
+++ b/dom/smil/SMILNullType.cpp
@@ -0,0 +1,57 @@
+/* -*- 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 "SMILNullType.h"
+
+#include "mozilla/SMILValue.h"
+#include "nsDebug.h"
+
+namespace mozilla {
+
+/*static*/
+SMILNullType* SMILNullType::Singleton() {
+ static SMILNullType sSingleton;
+ return &sSingleton;
+}
+
+nsresult SMILNullType::Assign(SMILValue& aDest, const SMILValue& aSrc) const {
+ MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aSrc.mType == this, "Unexpected source type");
+ aDest.mU = aSrc.mU;
+ aDest.mType = Singleton();
+ return NS_OK;
+}
+
+bool SMILNullType::IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const {
+ MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value");
+
+ return true; // All null-typed values are equivalent.
+}
+
+nsresult SMILNullType::Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const {
+ MOZ_ASSERT_UNREACHABLE("Adding NULL type");
+ return NS_ERROR_FAILURE;
+}
+
+nsresult SMILNullType::ComputeDistance(const SMILValue& aFrom,
+ const SMILValue& aTo,
+ double& aDistance) const {
+ MOZ_ASSERT_UNREACHABLE("Computing distance for NULL type");
+ return NS_ERROR_FAILURE;
+}
+
+nsresult SMILNullType::Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal,
+ double aUnitDistance,
+ SMILValue& aResult) const {
+ MOZ_ASSERT_UNREACHABLE("Interpolating NULL type");
+ return NS_ERROR_FAILURE;
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILNullType.h b/dom/smil/SMILNullType.h
new file mode 100644
index 0000000000..64448b3105
--- /dev/null
+++ b/dom/smil/SMILNullType.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 DOM_SMIL_SMILNULLTYPE_H_
+#define DOM_SMIL_SMILNULLTYPE_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILType.h"
+
+namespace mozilla {
+
+class SMILNullType : public SMILType {
+ public:
+ // Singleton for SMILValue objects to hold onto.
+ static SMILNullType* Singleton();
+
+ protected:
+ // SMILType Methods
+ // -------------------
+ virtual void Init(SMILValue& aValue) const override {}
+ virtual void Destroy(SMILValue& aValue) const override {}
+ virtual nsresult Assign(SMILValue& aDest,
+ const SMILValue& 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 SMILValue& aLeft,
+ const SMILValue& aRight) const override;
+ virtual nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const override;
+ virtual nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo,
+ double& aDistance) const override;
+ virtual nsresult Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal, double aUnitDistance,
+ SMILValue& aResult) const override;
+
+ private:
+ // Private constructor: prevent instances beyond my singleton.
+ constexpr SMILNullType() = default;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILNULLTYPE_H_
diff --git a/dom/smil/SMILParserUtils.cpp b/dom/smil/SMILParserUtils.cpp
new file mode 100644
index 0000000000..902b0ab181
--- /dev/null
+++ b/dom/smil/SMILParserUtils.cpp
@@ -0,0 +1,633 @@
+/* -*- 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 "SMILParserUtils.h"
+
+#include "mozilla/SMILAttr.h"
+#include "mozilla/SMILKeySpline.h"
+#include "mozilla/SMILRepeatCount.h"
+#include "mozilla/SMILTimeValue.h"
+#include "mozilla/SMILTimeValueSpecParams.h"
+#include "mozilla/SMILTypes.h"
+#include "mozilla/SMILValue.h"
+#include "mozilla/SVGContentUtils.h"
+#include "mozilla/TextUtils.h"
+#include "nsContentUtils.h"
+#include "nsCharSeparatedTokenizer.h"
+
+using namespace mozilla::dom;
+//------------------------------------------------------------------------------
+// Helper functions and Constants
+
+namespace {
+
+using namespace mozilla;
+
+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 u"accesskey("_ns // SMIL2+
+#define ACCESSKEY_PREFIX_CC u"accessKey("_ns // SVG/SMIL ANIM
+#define REPEAT_PREFIX u"repeat("_ns
+#define WALLCLOCK_PREFIX u"wallclock("_ns
+
+inline bool SkipWhitespace(RangedPtr<const char16_t>& aIter,
+ const RangedPtr<const char16_t>& aEnd) {
+ while (aIter != aEnd) {
+ if (!nsContentUtils::IsHTMLWhitespace(*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 || !mozilla::IsAsciiDigit(*aIter)) {
+ return false;
+ }
+
+ RangedPtr<const char16_t> iter(aIter);
+
+ if (++iter == aEnd || !mozilla::IsAsciiDigit(*iter)) {
+ return false;
+ }
+
+ uint32_t value = 10 * mozilla::AsciiAlphanumericToNumber(*aIter) +
+ mozilla::AsciiAlphanumericToNumber(*iter);
+ if (value > 59) {
+ return false;
+ }
+ if (++iter != aEnd && mozilla::IsAsciiDigit(*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,
+ SMILTimeValue* 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;
+ }
+ [[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(SMILTime(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(SMILTime(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,
+ SMILTimeValue* 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, SMILTimeValue* 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,
+ SMILTimeValue* aResult) {
+ if (aIter == aEnd) {
+ aResult->SetMillis(0L);
+ return true;
+ }
+
+ return SkipWhitespace(aIter, aEnd) && ParseOffsetValue(aIter, aEnd, aResult);
+}
+
+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 && !nsContentUtils::IsHTMLWhitespace(*aIter)) {
+ if (isCurrentCharEscaped) {
+ isCurrentCharEscaped = false;
+ } else {
+ if (*aIter == '+' || *aIter == '-' || (aBreakOnDot && *aIter == '.')) {
+ break;
+ }
+ if (*aIter == '\\') {
+ isCurrentCharEscaped = true;
+ aIsAnyCharEscaped = true;
+ }
+ }
+ ++aIter;
+ }
+}
+
+already_AddRefed<nsAtom> 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<nsAtom> 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,
+ SMILTimeValueSpecParams& aResult) {
+ SMILTimeValueSpecParams 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<nsAtom> 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 = SMILTimeValueSpecParams::SYNCBASE;
+ result.mSyncBegin = true;
+ // element-name.end
+ } else if (token2.EqualsLiteral("end")) {
+ result.mType = SMILTimeValueSpecParams::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 = SMILTimeValueSpecParams::REPEAT;
+ result.mRepeatIteration = repeatValue;
+ // element-name.event-symbol
+ } else {
+ atom = ConvertTokenToAtom(token2, requiresUnescaping);
+ if (atom == nullptr) {
+ return false;
+ }
+ result.mType = SMILTimeValueSpecParams::EVENT;
+ result.mEventSymbol = atom;
+ }
+ } else {
+ // event-symbol
+ result.mType = SMILTimeValueSpecParams::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
+
+namespace mozilla {
+
+//------------------------------------------------------------------------------
+// Implementation
+
+const nsDependentSubstring SMILParserUtils::TrimWhitespace(
+ const nsAString& aString) {
+ nsAString::const_iterator start, end;
+
+ aString.BeginReading(start);
+ aString.EndReading(end);
+
+ // Skip whitespace characters at the beginning
+ while (start != end && nsContentUtils::IsHTMLWhitespace(*start)) {
+ ++start;
+ }
+
+ // Skip whitespace characters at the end.
+ while (end != start) {
+ --end;
+
+ if (!nsContentUtils::IsHTMLWhitespace(*end)) {
+ // Step back to the last non-whitespace character.
+ ++end;
+
+ break;
+ }
+ }
+
+ return Substring(start, end);
+}
+
+bool SMILParserUtils::ParseKeySplines(
+ const nsAString& aSpec, FallibleTArray<SMILKeySpline>& aKeySplines) {
+ for (const auto& controlPoint :
+ nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace>(aSpec,
+ ';')
+ .ToRange()) {
+ nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace,
+ nsTokenizerFlags::SeparatorOptional>
+ tokenizer(controlPoint, ',');
+
+ double values[4];
+ for (auto& value : values) {
+ if (!tokenizer.hasMoreTokens() ||
+ !SVGContentUtils::ParseNumber(tokenizer.nextToken(), value) ||
+ value > 1.0 || value < 0.0) {
+ return false;
+ }
+ }
+ if (tokenizer.hasMoreTokens() || tokenizer.separatorAfterCurrentToken() ||
+ !aKeySplines.AppendElement(
+ SMILKeySpline(values[0], values[1], values[2], values[3]),
+ fallible)) {
+ return false;
+ }
+ }
+
+ return !aKeySplines.IsEmpty();
+}
+
+bool SMILParserUtils::ParseSemicolonDelimitedProgressList(
+ const nsAString& aSpec, bool aNonDecreasing,
+ FallibleTArray<double>& aArray) {
+ nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> 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 SMILParserUtils::GenericValueParser {
+ public:
+ SMILValueParser(const SVGAnimationElement* aSrcElement,
+ const SMILAttr* aSMILAttr,
+ FallibleTArray<SMILValue>* aValuesArray,
+ bool* aPreventCachingOfSandwich)
+ : mSrcElement(aSrcElement),
+ mSMILAttr(aSMILAttr),
+ mValuesArray(aValuesArray),
+ mPreventCachingOfSandwich(aPreventCachingOfSandwich) {}
+
+ virtual bool Parse(const nsAString& aValueStr) override {
+ SMILValue 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 SMILAttr* mSMILAttr;
+ FallibleTArray<SMILValue>* mValuesArray;
+ bool* mPreventCachingOfSandwich;
+};
+
+bool SMILParserUtils::ParseValues(const nsAString& aSpec,
+ const SVGAnimationElement* aSrcElement,
+ const SMILAttr& aAttribute,
+ FallibleTArray<SMILValue>& 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 SMILParserUtils::ParseValuesGeneric(const nsAString& aSpec,
+ GenericValueParser& aParser) {
+ nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tokenizer(
+ aSpec, ';');
+ if (!tokenizer.hasMoreTokens()) { // Empty list
+ return false;
+ }
+
+ while (tokenizer.hasMoreTokens()) {
+ if (!aParser.Parse(tokenizer.nextToken())) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool SMILParserUtils::ParseRepeatCount(const nsAString& aSpec,
+ SMILRepeatCount& aResult) {
+ const nsAString& spec = SMILParserUtils::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 SMILParserUtils::ParseTimeValueSpecParams(
+ const nsAString& aSpec, SMILTimeValueSpecParams& aResult) {
+ const nsAString& spec = TrimWhitespace(aSpec);
+
+ if (spec.EqualsLiteral("indefinite")) {
+ aResult.mType = SMILTimeValueSpecParams::INDEFINITE;
+ return true;
+ }
+
+ // offset type
+ if (ParseOffsetValue(spec, &aResult.mOffset)) {
+ aResult.mType = SMILTimeValueSpecParams::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 false; // accesskey is not supported
+ }
+
+ // event, syncbase, or repeat
+ return ParseElementBaseTimeValueSpec(spec, aResult);
+}
+
+bool SMILParserUtils::ParseClockValue(const nsAString& aSpec,
+ SMILTimeValue* 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 SMILParserUtils::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 && nsContentUtils::IsHTMLWhitespace(*iter)) {
+ ++iter;
+ }
+
+ // Check for dash
+ if (iter != end && *iter == '-') {
+ ++iter;
+ // Check for numeric character
+ if (iter != end && mozilla::IsAsciiDigit(*iter)) {
+ absValLocation = iter - start;
+ }
+ }
+ return absValLocation;
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILParserUtils.h b/dom/smil/SMILParserUtils.h
new file mode 100644
index 0000000000..bc17df051f
--- /dev/null
+++ b/dom/smil/SMILParserUtils.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 DOM_SMIL_SMILPARSERUTILS_H_
+#define DOM_SMIL_SMILPARSERUTILS_H_
+
+#include "nsTArray.h"
+#include "nsStringFwd.h"
+
+namespace mozilla {
+
+class SMILAttr;
+class SMILKeySpline;
+class SMILRepeatCount;
+class SMILTimeValue;
+class SMILTimeValueSpecParams;
+class SMILValue;
+
+namespace dom {
+class SVGAnimationElement;
+} // namespace dom
+
+/**
+ * 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 SMILParserUtils {
+ 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<SMILKeySpline>& 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 SMILAttr& aAttribute,
+ FallibleTArray<SMILValue>& 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,
+ SMILRepeatCount& aResult);
+
+ static bool ParseTimeValueSpecParams(const nsAString& aSpec,
+ SMILTimeValueSpecParams& 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, SMILTimeValue* 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);
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILPARSERUTILS_H_
diff --git a/dom/smil/SMILRepeatCount.cpp b/dom/smil/SMILRepeatCount.cpp
new file mode 100644
index 0000000000..9a56a42d45
--- /dev/null
+++ b/dom/smil/SMILRepeatCount.cpp
@@ -0,0 +1,14 @@
+/* -*- 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 "SMILRepeatCount.h"
+
+namespace mozilla {
+
+/*static*/ const double SMILRepeatCount::kNotSet = -1.0;
+/*static*/ const double SMILRepeatCount::kIndefinite = -2.0;
+
+} // namespace mozilla
diff --git a/dom/smil/SMILRepeatCount.h b/dom/smil/SMILRepeatCount.h
new file mode 100644
index 0000000000..0b93aae994
--- /dev/null
+++ b/dom/smil/SMILRepeatCount.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 DOM_SMIL_SMILREPEATCOUNT_H_
+#define DOM_SMIL_SMILREPEATCOUNT_H_
+
+#include "nsDebug.h"
+#include <math.h>
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// SMILRepeatCount
+//
+// 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 SMILRepeatCount {
+ public:
+ SMILRepeatCount() : mCount(kNotSet) {}
+ explicit SMILRepeatCount(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; }
+
+ SMILRepeatCount& 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;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILREPEATCOUNT_H_
diff --git a/dom/smil/SMILSetAnimationFunction.cpp b/dom/smil/SMILSetAnimationFunction.cpp
new file mode 100644
index 0000000000..38f3498c01
--- /dev/null
+++ b/dom/smil/SMILSetAnimationFunction.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 "SMILSetAnimationFunction.h"
+
+namespace mozilla {
+
+inline bool SMILSetAnimationFunction::IsDisallowedAttribute(
+ const nsAtom* 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)
+ //
+ return aAttribute == nsGkAtoms::calcMode || aAttribute == nsGkAtoms::values ||
+ aAttribute == nsGkAtoms::keyTimes ||
+ aAttribute == nsGkAtoms::keySplines || aAttribute == nsGkAtoms::from ||
+ aAttribute == nsGkAtoms::by || aAttribute == nsGkAtoms::additive ||
+ aAttribute == nsGkAtoms::accumulate;
+}
+
+bool SMILSetAnimationFunction::SetAttr(nsAtom* 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 SMILAnimationFunction::SetAttr(aAttribute, aValue, aResult,
+ aParseResult);
+}
+
+bool SMILSetAnimationFunction::UnsetAttr(nsAtom* aAttribute) {
+ if (IsDisallowedAttribute(aAttribute)) {
+ return true;
+ }
+
+ return SMILAnimationFunction::UnsetAttr(aAttribute);
+}
+
+bool SMILSetAnimationFunction::HasAttr(nsAtom* aAttName) const {
+ if (IsDisallowedAttribute(aAttName)) return false;
+
+ return SMILAnimationFunction::HasAttr(aAttName);
+}
+
+const nsAttrValue* SMILSetAnimationFunction::GetAttr(nsAtom* aAttName) const {
+ if (IsDisallowedAttribute(aAttName)) return nullptr;
+
+ return SMILAnimationFunction::GetAttr(aAttName);
+}
+
+bool SMILSetAnimationFunction::GetAttr(nsAtom* aAttName,
+ nsAString& aResult) const {
+ if (IsDisallowedAttribute(aAttName)) return false;
+
+ return SMILAnimationFunction::GetAttr(aAttName, aResult);
+}
+
+bool SMILSetAnimationFunction::WillReplace() const { return true; }
+
+} // namespace mozilla
diff --git a/dom/smil/SMILSetAnimationFunction.h b/dom/smil/SMILSetAnimationFunction.h
new file mode 100644
index 0000000000..0bc62e0aec
--- /dev/null
+++ b/dom/smil/SMILSetAnimationFunction.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/. */
+
+#ifndef DOM_SMIL_SMILSETANIMATIONFUNCTION_H_
+#define DOM_SMIL_SMILSETANIMATIONFUNCTION_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILAnimationFunction.h"
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// SMILSetAnimationFunction
+//
+// Subclass of SMILAnimationFunction that limits the behaviour to that offered
+// by a <set> element.
+//
+class SMILSetAnimationFunction : public SMILAnimationFunction {
+ 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(nsAtom* 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(nsAtom* 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(nsAtom* aAttName) const override;
+ virtual const nsAttrValue* GetAttr(nsAtom* aAttName) const override;
+ virtual bool GetAttr(nsAtom* aAttName, nsAString& aResult) const override;
+ virtual bool WillReplace() const override;
+
+ bool IsDisallowedAttribute(const nsAtom* aAttribute) const;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILSETANIMATIONFUNCTION_H_
diff --git a/dom/smil/SMILStringType.cpp b/dom/smil/SMILStringType.cpp
new file mode 100644
index 0000000000..a79a97eb2c
--- /dev/null
+++ b/dom/smil/SMILStringType.cpp
@@ -0,0 +1,75 @@
+/* -*- 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 "mozilla/SMILValue.h"
+#include "nsDebug.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+void SMILStringType::Init(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.IsNull(), "Unexpected value type");
+ aValue.mU.mPtr = new nsString();
+ aValue.mType = this;
+}
+
+void SMILStringType::Destroy(SMILValue& aValue) const {
+ MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value");
+ delete static_cast<nsAString*>(aValue.mU.mPtr);
+ aValue.mU.mPtr = nullptr;
+ aValue.mType = SMILNullType::Singleton();
+}
+
+nsresult SMILStringType::Assign(SMILValue& aDest, const SMILValue& aSrc) const {
+ MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(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 SMILValue& aLeft,
+ const SMILValue& aRight) const {
+ MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types");
+ MOZ_ASSERT(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(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const {
+ MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types");
+ MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type");
+ return NS_ERROR_FAILURE; // string values can't be added to each other
+}
+
+nsresult SMILStringType::ComputeDistance(const SMILValue& aFrom,
+ const SMILValue& aTo,
+ double& aDistance) const {
+ MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types");
+ MOZ_ASSERT(aFrom.mType == this, "Unexpected source type");
+ return NS_ERROR_FAILURE; // there is no concept of distance between string
+ // values
+}
+
+nsresult SMILStringType::Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal,
+ double aUnitDistance,
+ SMILValue& 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");
+ 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 0000000000..4bfbe5ebb4
--- /dev/null
+++ b/dom/smil/SMILStringType.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 DOM_SMIL_SMILSTRINGTYPE_H_
+#define DOM_SMIL_SMILSTRINGTYPE_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILType.h"
+
+namespace mozilla {
+
+class SMILStringType : public SMILType {
+ public:
+ // Singleton for SMILValue objects to hold onto.
+ static SMILStringType* Singleton() {
+ static SMILStringType sSingleton;
+ return &sSingleton;
+ }
+
+ protected:
+ // SMILType Methods
+ // -------------------
+ virtual void Init(SMILValue& aValue) const override;
+ virtual void Destroy(SMILValue& aValue) const override;
+ virtual nsresult Assign(SMILValue& aDest,
+ const SMILValue& aSrc) const override;
+ virtual bool IsEqual(const SMILValue& aLeft,
+ const SMILValue& aRight) const override;
+ virtual nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd,
+ uint32_t aCount) const override;
+ virtual nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo,
+ double& aDistance) const override;
+ virtual nsresult Interpolate(const SMILValue& aStartVal,
+ const SMILValue& aEndVal, double aUnitDistance,
+ SMILValue& aResult) const override;
+
+ private:
+ // Private constructor: prevent instances beyond my singleton.
+ constexpr SMILStringType() = default;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILSTRINGTYPE_H_
diff --git a/dom/smil/SMILTargetIdentifier.h b/dom/smil/SMILTargetIdentifier.h
new file mode 100644
index 0000000000..14a2ee3152
--- /dev/null
+++ b/dom/smil/SMILTargetIdentifier.h
@@ -0,0 +1,87 @@
+/* -*- 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 DOM_SMIL_SMILTARGETIDENTIFIER_H_
+#define DOM_SMIL_SMILTARGETIDENTIFIER_H_
+
+// XXX Avoid including this here by moving function bodies to the cpp file
+#include "nsAtom.h"
+#include "nsIContent.h"
+#include "mozilla/dom/Element.h"
+
+class nsIContent;
+
+namespace mozilla {
+namespace dom {
+class Element;
+}
+
+/**
+ * Struct: SMILTargetIdentifier
+ *
+ * Tuple of: { Animated Element, Attribute Name }
+ *
+ * Used in SMILAnimationController as hash key for mapping an animation
+ * target to the SMILCompositor for that target.
+ *
+ * NOTE: Need a nsRefPtr for the element & attribute name, because
+ * SMILAnimationController 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 SMILTargetIdentifier {
+ SMILTargetIdentifier()
+ : mElement(nullptr),
+ mAttributeName(nullptr),
+ mAttributeNamespaceID(kNameSpaceID_Unknown) {}
+
+ inline bool Equals(const SMILTargetIdentifier& aOther) const {
+ return (aOther.mElement == mElement &&
+ aOther.mAttributeName == mAttributeName &&
+ aOther.mAttributeNamespaceID == mAttributeNamespaceID);
+ }
+
+ RefPtr<mozilla::dom::Element> mElement;
+ RefPtr<nsAtom> mAttributeName;
+ int32_t mAttributeNamespaceID;
+};
+
+/**
+ * Class: SMILWeakTargetIdentifier
+ *
+ * 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 SMILWeakTargetIdentifier {
+ public:
+ // Trivial constructor
+ SMILWeakTargetIdentifier() : mElement(nullptr), mAttributeName(nullptr) {}
+
+ // Allow us to update a weak identifier to match a given non-weak identifier
+ SMILWeakTargetIdentifier& operator=(const SMILTargetIdentifier& aOther) {
+ mElement = aOther.mElement;
+ mAttributeName = aOther.mAttributeName;
+ return *this;
+ }
+
+ // Allow for comparison vs. non-weak identifier
+ inline bool Equals(const SMILTargetIdentifier& aOther) const {
+ return (aOther.mElement == mElement &&
+ aOther.mAttributeName == mAttributeName);
+ }
+
+ private:
+ const nsIContent* mElement;
+ const nsAtom* mAttributeName;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILTARGETIDENTIFIER_H_
diff --git a/dom/smil/SMILTimeContainer.cpp b/dom/smil/SMILTimeContainer.cpp
new file mode 100644
index 0000000000..cc2101cd6b
--- /dev/null
+++ b/dom/smil/SMILTimeContainer.cpp
@@ -0,0 +1,301 @@
+/* -*- 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 "SMILTimeContainer.h"
+
+#include "mozilla/AutoRestore.h"
+#include "mozilla/SMILTimedElement.h"
+#include "mozilla/SMILTimeValue.h"
+#include <algorithm>
+
+namespace mozilla {
+
+SMILTimeContainer::SMILTimeContainer()
+ : mParent(nullptr),
+ mCurrentTime(0L),
+ mParentOffset(0L),
+ mPauseStart(0L),
+ mNeedsPauseSample(false),
+ mNeedsRewind(false),
+ mIsSeeking(false),
+#ifdef DEBUG
+ mHoldingEntries(false),
+#endif
+ mPauseState(PAUSE_BEGIN) {
+}
+
+SMILTimeContainer::~SMILTimeContainer() {
+ if (mParent) {
+ mParent->RemoveChild(*this);
+ }
+}
+
+SMILTimeValue SMILTimeContainer::ContainerToParentTime(
+ SMILTime aContainerTime) const {
+ // If we're paused, then future times are indefinite
+ if (IsPaused() && aContainerTime > mCurrentTime)
+ return SMILTimeValue::Indefinite();
+
+ return SMILTimeValue(aContainerTime + mParentOffset);
+}
+
+SMILTimeValue SMILTimeContainer::ParentToContainerTime(
+ SMILTime aParentTime) const {
+ // If we're paused, then any time after when we paused is indefinite
+ if (IsPaused() && aParentTime > mPauseStart)
+ return SMILTimeValue::Indefinite();
+
+ return SMILTimeValue(aParentTime - mParentOffset);
+}
+
+void SMILTimeContainer::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 SMILTimeContainer::Pause(uint32_t aType) {
+ bool didStartPause = false;
+
+ if (!mPauseState && aType) {
+ mPauseStart = GetParentTime();
+ mNeedsPauseSample = true;
+ didStartPause = true;
+ }
+
+ mPauseState |= aType;
+
+ if (didStartPause) {
+ NotifyTimeChange();
+ }
+}
+
+void SMILTimeContainer::Resume(uint32_t aType) {
+ if (!mPauseState) return;
+
+ mPauseState &= ~aType;
+
+ if (!mPauseState) {
+ SMILTime extraOffset = GetParentTime() - mPauseStart;
+ mParentOffset += extraOffset;
+ NotifyTimeChange();
+ }
+}
+
+SMILTime SMILTimeContainer::GetCurrentTimeAsSMILTime() 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 SMILTimeContainer::SetCurrentTime(SMILTime 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<SMILTime>(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.
+ SMILTime 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();
+}
+
+SMILTime SMILTimeContainer::GetParentTime() const {
+ if (mParent) return mParent->GetCurrentTimeAsSMILTime();
+
+ return 0L;
+}
+
+void SMILTimeContainer::SyncPauseTime() {
+ if (IsPaused()) {
+ SMILTime parentTime = GetParentTime();
+ SMILTime extraOffset = parentTime - mPauseStart;
+ mParentOffset += extraOffset;
+ mPauseStart = parentTime;
+ }
+}
+
+void SMILTimeContainer::Sample() {
+ if (!NeedsSample()) return;
+
+ UpdateCurrentTime();
+ DoSample();
+
+ mNeedsPauseSample = false;
+}
+
+nsresult SMILTimeContainer::SetParent(SMILTimeContainer* 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;
+}
+
+void SMILTimeContainer::AddMilestone(
+ const SMILMilestone& 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_ASSERT(!mHoldingEntries);
+ mMilestoneEntries.Push(MilestoneEntry(aMilestone, aElement));
+}
+
+void SMILTimeContainer::ClearMilestones() {
+ MOZ_ASSERT(!mHoldingEntries);
+ mMilestoneEntries.Clear();
+}
+
+bool SMILTimeContainer::GetNextMilestoneInParentTime(
+ SMILMilestone& aNextMilestone) const {
+ if (mMilestoneEntries.IsEmpty()) return false;
+
+ SMILTimeValue parentTime =
+ ContainerToParentTime(mMilestoneEntries.Top().mMilestone.mTime);
+ if (!parentTime.IsDefinite()) return false;
+
+ aNextMilestone = SMILMilestone(parentTime.GetMillis(),
+ mMilestoneEntries.Top().mMilestone.mIsEnd);
+
+ return true;
+}
+
+bool SMILTimeContainer::PopMilestoneElementsAtMilestone(
+ const SMILMilestone& aMilestone, AnimElemArray& aMatchedElements) {
+ if (mMilestoneEntries.IsEmpty()) return false;
+
+ SMILTimeValue containerTime = ParentToContainerTime(aMilestone.mTime);
+ if (!containerTime.IsDefinite()) return false;
+
+ SMILMilestone 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_ASSERT(!mHoldingEntries);
+
+ bool gotOne = false;
+ while (!mMilestoneEntries.IsEmpty() &&
+ mMilestoneEntries.Top().mMilestone == containerMilestone) {
+ aMatchedElements.AppendElement(mMilestoneEntries.Pop().mTimebase);
+ gotOne = true;
+ }
+
+ return gotOne;
+}
+
+void SMILTimeContainer::Traverse(
+ nsCycleCollectionTraversalCallback* aCallback) {
+#ifdef DEBUG
+ AutoRestore<bool> saveHolding(mHoldingEntries);
+ mHoldingEntries = true;
+#endif
+ 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 SMILTimeContainer::Unlink() {
+ MOZ_ASSERT(!mHoldingEntries);
+ mMilestoneEntries.Clear();
+}
+
+void SMILTimeContainer::UpdateCurrentTime() {
+ SMILTime now = IsPaused() ? mPauseStart : GetParentTime();
+ mCurrentTime = now - mParentOffset;
+ MOZ_ASSERT(mCurrentTime >= 0, "Container has negative time");
+}
+
+void SMILTimeContainer::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;
+
+ {
+#ifdef DEBUG
+ AutoRestore<bool> saveHolding(mHoldingEntries);
+ mHoldingEntries = true;
+#endif
+ for (const MilestoneEntry* p = mMilestoneEntries.Elements();
+ p < mMilestoneEntries.Elements() + mMilestoneEntries.Length(); ++p) {
+ elems.AppendElement(p->mTimebase.get());
+ }
+ }
+
+ for (auto& elem : elems) {
+ elem->TimedElement().HandleContainerTimeChange();
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILTimeContainer.h b/dom/smil/SMILTimeContainer.h
new file mode 100644
index 0000000000..62ea7ad2c4
--- /dev/null
+++ b/dom/smil/SMILTimeContainer.h
@@ -0,0 +1,299 @@
+/* -*- 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 DOM_SMIL_SMILTIMECONTAINER_H_
+#define DOM_SMIL_SMILTIMECONTAINER_H_
+
+#include "mozilla/dom/SVGAnimationElement.h"
+#include "mozilla/SMILMilestone.h"
+#include "mozilla/SMILTypes.h"
+#include "nscore.h"
+#include "nsTPriorityQueue.h"
+
+namespace mozilla {
+
+class SMILTimeValue;
+
+//----------------------------------------------------------------------
+// SMILTimeContainer
+//
+// Common base class for a time base that can be paused, resumed, and sampled.
+//
+class SMILTimeContainer {
+ public:
+ SMILTimeContainer();
+ virtual ~SMILTimeContainer();
+
+ /*
+ * 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.
+ */
+ SMILTime GetCurrentTimeAsSMILTime() 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(SMILTime aSeekTo);
+
+ /*
+ * Return the current time for the parent time container if any.
+ */
+ virtual SMILTime 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.
+ */
+ SMILTimeValue ContainerToParentTime(SMILTime 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.
+ */
+ SMILTimeValue ParentToContainerTime(SMILTime 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(SMILTimeContainer* 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.
+ */
+ void AddMilestone(const SMILMilestone& 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(SMILMilestone& aNextMilestone) const;
+
+ using AnimElemArray = nsTArray<RefPtr<dom::SVGAnimationElement>>;
+
+ /*
+ * 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 SMILMilestone& 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(SMILTimeContainer& aChild) {
+ return NS_ERROR_FAILURE;
+ }
+
+ /*
+ * Removes a child time container.
+ */
+ virtual void RemoveChild(SMILTimeContainer& 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
+ SMILTimeContainer* mParent;
+
+ // The current time established at the last call to Sample()
+ SMILTime 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
+ //
+ SMILTime mParentOffset;
+
+ // The timestamp in parent time when the container was paused
+ SMILTime 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
+
+#ifdef DEBUG
+ bool mHoldingEntries; // True if there's a raw pointer to mMilestoneEntries
+ // on the stack.
+#endif
+
+ // A bitfield of the pause state for all pause requests
+ uint32_t mPauseState;
+
+ struct MilestoneEntry {
+ MilestoneEntry(const SMILMilestone& aMilestone,
+ mozilla::dom::SVGAnimationElement& aElement)
+ : mMilestone(aMilestone), mTimebase(&aElement) {}
+
+ bool operator<(const MilestoneEntry& aOther) const {
+ return mMilestone < aOther.mMilestone;
+ }
+
+ SMILMilestone 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;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILTIMECONTAINER_H_
diff --git a/dom/smil/SMILTimeValue.cpp b/dom/smil/SMILTimeValue.cpp
new file mode 100644
index 0000000000..c198a74013
--- /dev/null
+++ b/dom/smil/SMILTimeValue.cpp
@@ -0,0 +1,42 @@
+/* -*- 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 "SMILTimeValue.h"
+
+namespace mozilla {
+
+const SMILTime SMILTimeValue::kUnresolvedMillis =
+ std::numeric_limits<SMILTime>::max();
+
+//----------------------------------------------------------------------
+// SMILTimeValue methods:
+
+static inline int8_t Cmp(int64_t aA, int64_t aB) {
+ return aA == aB ? 0 : (aA > aB ? 1 : -1);
+}
+
+int8_t SMILTimeValue::CompareTo(const SMILTimeValue& 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;
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILTimeValue.h b/dom/smil/SMILTimeValue.h
new file mode 100644
index 0000000000..0fbe7d3b36
--- /dev/null
+++ b/dom/smil/SMILTimeValue.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 DOM_SMIL_SMILTIMEVALUE_H_
+#define DOM_SMIL_SMILTIMEVALUE_H_
+
+#include "mozilla/SMILTypes.h"
+#include "nsDebug.h"
+
+namespace mozilla {
+
+/*----------------------------------------------------------------------
+ * SMILTimeValue class
+ *
+ * A tri-state time value.
+ *
+ * First a quick overview of the SMIL time data types:
+ *
+ * SMILTime -- a timestamp in milliseconds.
+ * SMILTimeValue -- (this class) a timestamp that can take the additional
+ * states 'indefinite' and 'unresolved'
+ * SMILInstanceTime -- an SMILTimeValue used for constructing intervals. It
+ * contains additional fields to govern reset behavior
+ * and track timing dependencies (e.g. syncbase timing).
+ * SMILInterval -- a pair of SMILInstanceTimes that defines a begin and
+ * an end time for animation.
+ * SMILTimeValueSpec -- 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 SMILTimedElement and its
+ * SMILInstanceTimes 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 | SMILTimeValue | true | false | true
+ * -----------+---------------+------------+--------------+------------
+ * Indefinite | -- | false | true | true
+ * -----------+---------------+------------+--------------+------------
+ * Unresolved | -- | false | false | false
+ *
+ */
+
+class SMILTimeValue {
+ public:
+ // Creates an unresolved time value
+ SMILTimeValue()
+ : mMilliseconds(kUnresolvedMillis), mState(STATE_UNRESOLVED) {}
+
+ // Creates a resolved time value
+ explicit SMILTimeValue(SMILTime aMillis)
+ : mMilliseconds(aMillis), mState(STATE_DEFINITE) {}
+
+ // Named constructor to create an indefinite time value
+ static SMILTimeValue Indefinite() {
+ SMILTimeValue 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; }
+ SMILTime GetMillis() const {
+ MOZ_ASSERT(mState == STATE_DEFINITE,
+ "GetMillis() called for unresolved or indefinite time");
+
+ return mState == STATE_DEFINITE ? mMilliseconds : kUnresolvedMillis;
+ }
+
+ void SetMillis(SMILTime aMillis) {
+ mState = STATE_DEFINITE;
+ mMilliseconds = aMillis;
+ }
+
+ int8_t CompareTo(const SMILTimeValue& aOther) const;
+
+ bool operator==(const SMILTimeValue& aOther) const {
+ return CompareTo(aOther) == 0;
+ }
+
+ bool operator!=(const SMILTimeValue& aOther) const {
+ return CompareTo(aOther) != 0;
+ }
+
+ bool operator<(const SMILTimeValue& aOther) const {
+ return CompareTo(aOther) < 0;
+ }
+
+ bool operator>(const SMILTimeValue& aOther) const {
+ return CompareTo(aOther) > 0;
+ }
+
+ bool operator<=(const SMILTimeValue& aOther) const {
+ return CompareTo(aOther) <= 0;
+ }
+
+ bool operator>=(const SMILTimeValue& aOther) const {
+ return CompareTo(aOther) >= 0;
+ }
+
+ private:
+ static const SMILTime kUnresolvedMillis;
+
+ SMILTime mMilliseconds;
+ enum { STATE_DEFINITE, STATE_INDEFINITE, STATE_UNRESOLVED } mState;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILTIMEVALUE_H_
diff --git a/dom/smil/SMILTimeValueSpec.cpp b/dom/smil/SMILTimeValueSpec.cpp
new file mode 100644
index 0000000000..3ce58e9598
--- /dev/null
+++ b/dom/smil/SMILTimeValueSpec.cpp
@@ -0,0 +1,370 @@
+/* -*- 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/SMILInstanceTime.h"
+#include "mozilla/SMILInterval.h"
+#include "mozilla/SMILParserUtils.h"
+#include "mozilla/SMILTimeContainer.h"
+#include "mozilla/SMILTimedElement.h"
+#include "mozilla/SMILTimeValueSpec.h"
+#include "mozilla/SMILTimeValue.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/SVGAnimationElement.h"
+#include "mozilla/dom/TimeEvent.h"
+#include "nsString.h"
+#include <limits>
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// Nested class: EventListener
+
+NS_IMPL_ISUPPORTS(SMILTimeValueSpec::EventListener, nsIDOMEventListener)
+
+NS_IMETHODIMP
+SMILTimeValueSpec::EventListener::HandleEvent(Event* aEvent) {
+ if (mSpec) {
+ mSpec->HandleEvent(aEvent);
+ }
+ return NS_OK;
+}
+
+//----------------------------------------------------------------------
+// Implementation
+
+SMILTimeValueSpec::SMILTimeValueSpec(SMILTimedElement& aOwner, bool aIsBegin)
+ : mOwner(&aOwner), mIsBegin(aIsBegin), mReferencedElement(this) {}
+
+SMILTimeValueSpec::~SMILTimeValueSpec() {
+ UnregisterFromReferencedElement(mReferencedElement.get());
+ if (mEventListener) {
+ mEventListener->Disconnect();
+ mEventListener = nullptr;
+ }
+}
+
+nsresult SMILTimeValueSpec::SetSpec(const nsAString& aStringSpec,
+ Element& aContextElement) {
+ SMILTimeValueSpecParams params;
+
+ if (!SMILParserUtils::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 == SMILTimeValueSpecParams::OFFSET ||
+ (!mIsBegin && mParams.mType == SMILTimeValueSpecParams::INDEFINITE)) {
+ mOwner->AddInstanceTime(new SMILInstanceTime(mParams.mOffset), mIsBegin);
+ }
+
+ // Fill in the event symbol to simplify handling later
+ if (mParams.mType == SMILTimeValueSpecParams::REPEAT) {
+ mParams.mEventSymbol = nsGkAtoms::repeatEvent;
+ }
+
+ ResolveReferences(aContextElement);
+
+ return NS_OK;
+}
+
+void SMILTimeValueSpec::ResolveReferences(Element& aContextElement) {
+ if (mParams.mType != SMILTimeValueSpecParams::SYNCBASE && !IsEventBased()) {
+ return;
+ }
+
+ // If we're not bound to the document yet, don't worry, we'll get called again
+ // when that happens
+ if (!aContextElement.IsInComposedDoc()) 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(aContextElement, mParams.mDependentElemID);
+ } else if (mParams.mType == SMILTimeValueSpecParams::EVENT) {
+ Element* target = mOwner->GetTargetElement();
+ mReferencedElement.ResetWithElement(target);
+ } else {
+ MOZ_ASSERT(false, "Syncbase or repeat spec without ID");
+ }
+ UpdateReferencedElement(oldReferencedElement, mReferencedElement.get());
+}
+
+bool SMILTimeValueSpec::IsEventBased() const {
+ return mParams.mType == SMILTimeValueSpecParams::EVENT ||
+ mParams.mType == SMILTimeValueSpecParams::REPEAT;
+}
+
+void SMILTimeValueSpec::HandleNewInterval(
+ SMILInterval& aInterval, const SMILTimeContainer* aSrcContainer) {
+ const SMILInstanceTime& baseInstance =
+ mParams.mSyncBegin ? *aInterval.Begin() : *aInterval.End();
+ SMILTimeValue newTime =
+ ConvertBetweenTimeContainers(baseInstance.Time(), aSrcContainer);
+
+ // Apply offset
+ if (!ApplyOffset(newTime)) {
+ NS_WARNING("New time overflows SMILTime, ignoring");
+ return;
+ }
+
+ // Create the instance time and register it with the interval
+ RefPtr<SMILInstanceTime> newInstance = new SMILInstanceTime(
+ newTime, SMILInstanceTime::SOURCE_SYNCBASE, this, &aInterval);
+ mOwner->AddInstanceTime(newInstance, mIsBegin);
+}
+
+void SMILTimeValueSpec::HandleTargetElementChange(Element* aNewTarget) {
+ if (!IsEventBased() || mParams.mDependentElemID) return;
+
+ mReferencedElement.ResetWithElement(aNewTarget);
+}
+
+void SMILTimeValueSpec::HandleChangedInstanceTime(
+ const SMILInstanceTime& aBaseTime, const SMILTimeContainer* aSrcContainer,
+ SMILInstanceTime& 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;
+
+ SMILTimeValue updatedTime =
+ ConvertBetweenTimeContainers(aBaseTime.Time(), aSrcContainer);
+
+ // Apply offset
+ if (!ApplyOffset(updatedTime)) {
+ NS_WARNING("Updated time overflows SMILTime, 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 SMILTimeValueSpec::HandleDeletedInstanceTime(
+ SMILInstanceTime& aInstanceTime) {
+ mOwner->RemoveInstanceTime(&aInstanceTime, mIsBegin);
+}
+
+bool SMILTimeValueSpec::DependsOnBegin() const { return mParams.mSyncBegin; }
+
+void SMILTimeValueSpec::Traverse(
+ nsCycleCollectionTraversalCallback* aCallback) {
+ mReferencedElement.Traverse(aCallback);
+}
+
+void SMILTimeValueSpec::Unlink() {
+ UnregisterFromReferencedElement(mReferencedElement.get());
+ mReferencedElement.Unlink();
+}
+
+//----------------------------------------------------------------------
+// Implementation helpers
+
+void SMILTimeValueSpec::UpdateReferencedElement(Element* aFrom, Element* aTo) {
+ if (aFrom == aTo) return;
+
+ UnregisterFromReferencedElement(aFrom);
+
+ switch (mParams.mType) {
+ case SMILTimeValueSpecParams::SYNCBASE: {
+ SMILTimedElement* to = GetTimedElement(aTo);
+ if (to) {
+ to->AddDependent(*this);
+ }
+ } break;
+
+ case SMILTimeValueSpecParams::EVENT:
+ case SMILTimeValueSpecParams::REPEAT:
+ RegisterEventListener(aTo);
+ break;
+
+ default:
+ // not a referencing-type
+ break;
+ }
+}
+
+void SMILTimeValueSpec::UnregisterFromReferencedElement(Element* aElement) {
+ if (!aElement) return;
+
+ if (mParams.mType == SMILTimeValueSpecParams::SYNCBASE) {
+ SMILTimedElement* timedElement = GetTimedElement(aElement);
+ if (timedElement) {
+ timedElement->RemoveDependent(*this);
+ }
+ mOwner->RemoveInstanceTimesForCreator(this, mIsBegin);
+ } else if (IsEventBased()) {
+ UnregisterEventListener(aElement);
+ }
+}
+
+SMILTimedElement* SMILTimeValueSpec::GetTimedElement(Element* aElement) {
+ nsCOMPtr<SVGAnimationElement> animationElement = do_QueryInterface(aElement);
+ return animationElement ? &animationElement->TimedElement() : nullptr;
+}
+
+// Indicates whether we're allowed to register an event-listener
+// when scripting is disabled.
+bool SMILTimeValueSpec::IsWhitelistedEvent() {
+ // The category of (SMIL-specific) "repeat(n)" events are allowed.
+ if (mParams.mType == SMILTimeValueSpecParams::REPEAT) {
+ return true;
+ }
+
+ // A specific list of other SMIL-related events are allowed, too.
+ if (mParams.mType == SMILTimeValueSpecParams::EVENT &&
+ (mParams.mEventSymbol == nsGkAtoms::repeat ||
+ mParams.mEventSymbol == nsGkAtoms::repeatEvent ||
+ mParams.mEventSymbol == nsGkAtoms::beginEvent ||
+ mParams.mEventSymbol == nsGkAtoms::endEvent)) {
+ return true;
+ }
+
+ return false;
+}
+
+void SMILTimeValueSpec::RegisterEventListener(Element* aTarget) {
+ MOZ_ASSERT(IsEventBased(),
+ "Attempting to register event-listener for unexpected "
+ "SMILTimeValueSpec 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 = aTarget->GetOrCreateListenerManager();
+ if (!elm) {
+ return;
+ }
+
+ elm->AddEventListenerByType(mEventListener,
+ nsDependentAtomString(mParams.mEventSymbol),
+ AllEventsAtSystemGroupBubble());
+}
+
+void SMILTimeValueSpec::UnregisterEventListener(Element* aTarget) {
+ if (!aTarget || !mEventListener) {
+ return;
+ }
+
+ EventListenerManager* elm = aTarget->GetOrCreateListenerManager();
+ if (!elm) {
+ return;
+ }
+
+ elm->RemoveEventListenerByType(mEventListener,
+ nsDependentAtomString(mParams.mEventSymbol),
+ AllEventsAtSystemGroupBubble());
+}
+
+void SMILTimeValueSpec::HandleEvent(Event* aEvent) {
+ MOZ_ASSERT(mEventListener, "Got event without an event listener");
+ MOZ_ASSERT(IsEventBased(), "Got event for non-event SMILTimeValueSpec");
+ 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
+ SMILTimeContainer* container = mOwner->GetTimeContainer();
+ if (!container) return;
+
+ if (mParams.mType == SMILTimeValueSpecParams::REPEAT &&
+ !CheckRepeatEventDetail(aEvent)) {
+ return;
+ }
+
+ SMILTime currentTime = container->GetCurrentTimeAsSMILTime();
+ SMILTimeValue newTime(currentTime);
+ if (!ApplyOffset(newTime)) {
+ NS_WARNING("New time generated from event overflows SMILTime, ignoring");
+ return;
+ }
+
+ RefPtr<SMILInstanceTime> newInstance =
+ new SMILInstanceTime(newTime, SMILInstanceTime::SOURCE_EVENT);
+ mOwner->AddInstanceTime(newInstance, mIsBegin);
+}
+
+bool SMILTimeValueSpec::CheckRepeatEventDetail(Event* aEvent) {
+ TimeEvent* timeEvent = aEvent->AsTimeEvent();
+ if (!timeEvent) {
+ NS_WARNING("Received a repeat event that was not a DOMTimeEvent");
+ return false;
+ }
+
+ int32_t detail = timeEvent->Detail();
+ return detail > 0 && (uint32_t)detail == mParams.mRepeatIteration;
+}
+
+SMILTimeValue SMILTimeValueSpec::ConvertBetweenTimeContainers(
+ const SMILTimeValue& aSrcTime, const SMILTimeContainer* 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 SMILTimeContainer* 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 SMILTimeValue(); // unresolved
+
+ SMILTimeValue 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 SMILTimeValueSpec::ApplyOffset(SMILTimeValue& aTime) const {
+ // indefinite + offset = indefinite. Likewise for unresolved times.
+ if (!aTime.IsDefinite()) {
+ return true;
+ }
+
+ double resultAsDouble =
+ (double)aTime.GetMillis() + mParams.mOffset.GetMillis();
+ if (resultAsDouble > double(std::numeric_limits<SMILTime>::max()) ||
+ resultAsDouble < double(std::numeric_limits<SMILTime>::min())) {
+ return false;
+ }
+ aTime.SetMillis(aTime.GetMillis() + mParams.mOffset.GetMillis());
+ return true;
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILTimeValueSpec.h b/dom/smil/SMILTimeValueSpec.h
new file mode 100644
index 0000000000..e00dcf31be
--- /dev/null
+++ b/dom/smil/SMILTimeValueSpec.h
@@ -0,0 +1,143 @@
+/* -*- 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 DOM_SMIL_SMILTIMEVALUESPEC_H_
+#define DOM_SMIL_SMILTIMEVALUESPEC_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/SMILTimeValueSpecParams.h"
+#include "mozilla/dom/IDTracker.h"
+#include "nsStringFwd.h"
+#include "nsIDOMEventListener.h"
+
+// XXX Avoid including this here by moving function bodies to the cpp file
+#include "mozilla/dom/Element.h"
+
+namespace mozilla {
+
+class EventListenerManager;
+class SMILInstanceTime;
+class SMILInterval;
+class SMILTimeContainer;
+class SMILTimedElement;
+class SMILTimeValue;
+
+namespace dom {
+class Event;
+} // namespace dom
+
+//----------------------------------------------------------------------
+// SMILTimeValueSpec 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 and repeat specifications)
+// and synchronisation (for syncbase specifications).
+//
+// For an overview of how this class is related to other SMIL time classes see
+// the documentation in SMILTimeValue.h
+
+class SMILTimeValueSpec {
+ public:
+ using Element = dom::Element;
+ using Event = dom::Event;
+ using IDTracker = dom::IDTracker;
+
+ SMILTimeValueSpec(SMILTimedElement& aOwner, bool aIsBegin);
+ ~SMILTimeValueSpec();
+
+ nsresult SetSpec(const nsAString& aStringSpec, Element& aContextElement);
+ void ResolveReferences(Element& aContextElement);
+ bool IsEventBased() const;
+
+ void HandleNewInterval(SMILInterval& aInterval,
+ const SMILTimeContainer* aSrcContainer);
+ void HandleTargetElementChange(Element* aNewTarget);
+
+ // For created SMILInstanceTime objects
+ bool DependsOnBegin() const;
+ void HandleChangedInstanceTime(const SMILInstanceTime& aBaseTime,
+ const SMILTimeContainer* aSrcContainer,
+ SMILInstanceTime& aInstanceTimeToUpdate,
+ bool aObjectChanged);
+ void HandleDeletedInstanceTime(SMILInstanceTime& aInstanceTime);
+
+ // Cycle-collection support
+ void Traverse(nsCycleCollectionTraversalCallback* aCallback);
+ void Unlink();
+
+ protected:
+ void UpdateReferencedElement(Element* aFrom, Element* aTo);
+ void UnregisterFromReferencedElement(Element* aElement);
+ SMILTimedElement* GetTimedElement(Element* aElement);
+ bool IsWhitelistedEvent();
+ void RegisterEventListener(Element* aTarget);
+ void UnregisterEventListener(Element* aTarget);
+ void HandleEvent(Event* aEvent);
+ bool CheckRepeatEventDetail(Event* aEvent);
+ SMILTimeValue ConvertBetweenTimeContainers(
+ const SMILTimeValue& aSrcTime, const SMILTimeContainer* aSrcContainer);
+ bool ApplyOffset(SMILTimeValue& aTime) const;
+
+ SMILTimedElement* 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.
+ SMILTimeValueSpecParams mParams;
+
+ /**
+ * If our SMILTimeValueSpec exists for a 'begin' or 'end' attribute with a
+ * value that specifies a time that is relative to the animation of some
+ * other element, it will create an instance of this class to reference and
+ * track that other element. For example, if the SMILTimeValueSpec is for
+ * end='a.end+2s', an instance of this class will be created to track the
+ * element associated with the element ID "a". This class will notify the
+ * SMILTimeValueSpec if the element that that ID identifies changes to a
+ * different element (or none).
+ */
+ class TimeReferenceTracker final : public IDTracker {
+ public:
+ explicit TimeReferenceTracker(SMILTimeValueSpec* aOwner) : mSpec(aOwner) {}
+ void ResetWithElement(Element* aTo) {
+ RefPtr<Element> from = get();
+ Unlink();
+ ElementChanged(from, aTo);
+ }
+
+ protected:
+ virtual void ElementChanged(Element* aFrom, Element* aTo) override {
+ IDTracker::ElementChanged(aFrom, aTo);
+ mSpec->UpdateReferencedElement(aFrom, aTo);
+ }
+ virtual bool IsPersistent() override { return true; }
+
+ private:
+ SMILTimeValueSpec* mSpec;
+ };
+
+ TimeReferenceTracker mReferencedElement;
+
+ class EventListener final : public nsIDOMEventListener {
+ ~EventListener() {}
+
+ public:
+ explicit EventListener(SMILTimeValueSpec* aOwner) : mSpec(aOwner) {}
+ void Disconnect() { mSpec = nullptr; }
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIDOMEVENTLISTENER
+
+ private:
+ SMILTimeValueSpec* mSpec;
+ };
+ RefPtr<EventListener> mEventListener;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILTIMEVALUESPEC_H_
diff --git a/dom/smil/SMILTimeValueSpecParams.h b/dom/smil/SMILTimeValueSpecParams.h
new file mode 100644
index 0000000000..2487168b7f
--- /dev/null
+++ b/dom/smil/SMILTimeValueSpecParams.h
@@ -0,0 +1,58 @@
+/* -*- 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 DOM_SMIL_SMILTIMEVALUESPECPARAMS_H_
+#define DOM_SMIL_SMILTIMEVALUESPECPARAMS_H_
+
+#include "mozilla/SMILTimeValue.h"
+#include "nsAtom.h"
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// SMILTimeValueSpecParams
+//
+// 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 SMILTimeValueSpecParams {
+ public:
+ SMILTimeValueSpecParams()
+ : mType(INDEFINITE), mSyncBegin(false), mRepeatIteration(0) {}
+
+ // The type of value this specification describes
+ enum { OFFSET, SYNCBASE, EVENT, REPEAT, 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
+ // It is not used for WALLCLOCK or INDEFINITE times
+ SMILTimeValue 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<nsAtom> mDependentElemID;
+
+ // The event to respond to.
+ // Only used for EVENT types.
+ RefPtr<nsAtom> 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 to respond to.
+ // Only used for mType=REPEAT.
+ uint32_t mRepeatIteration;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILTIMEVALUESPECPARAMS_H_
diff --git a/dom/smil/SMILTimedElement.cpp b/dom/smil/SMILTimedElement.cpp
new file mode 100644
index 0000000000..a5999fd76a
--- /dev/null
+++ b/dom/smil/SMILTimedElement.cpp
@@ -0,0 +1,2180 @@
+/* -*- 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 "SMILTimedElement.h"
+
+#include "mozilla/AutoRestore.h"
+#include "mozilla/ContentEvents.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/SMILAnimationFunction.h"
+#include "mozilla/SMILInstanceTime.h"
+#include "mozilla/SMILParserUtils.h"
+#include "mozilla/SMILTimeContainer.h"
+#include "mozilla/SMILTimeValue.h"
+#include "mozilla/SMILTimeValueSpec.h"
+#include "mozilla/TaskCategory.h"
+#include "mozilla/dom/DocumentInlines.h"
+#include "mozilla/dom/SVGAnimationElement.h"
+#include "nsAttrValueInlines.h"
+#include "nsGkAtoms.h"
+#include "nsReadableUtils.h"
+#include "nsMathUtils.h"
+#include "nsThreadUtils.h"
+#include "prdtoa.h"
+#include "plstr.h"
+#include "prtime.h"
+#include "nsString.h"
+#include "nsCharSeparatedTokenizer.h"
+#include <algorithm>
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// 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 SMILTimedElement::InstanceTimeComparator::Equals(
+ const SMILInstanceTime* aElem1, const SMILInstanceTime* 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 SMILTimedElement::InstanceTimeComparator::LessThan(
+ const SMILInstanceTime* aElem1, const SMILInstanceTime* 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)
+ : mozilla::Runnable("AsyncTimeEventRunner"),
+ mTarget(aTarget),
+ mMsg(aMsg),
+ mDetail(aDetail) {}
+
+ NS_IMETHOD Run() override {
+ InternalSMILTimeEvent event(true, mMsg);
+ event.mDetail = mDetail;
+
+ nsPresContext* context = nullptr;
+ Document* doc = mTarget->GetComposedDoc();
+ if (doc) {
+ context = doc->GetPresContext();
+ }
+
+ return EventDispatcher::Dispatch(mTarget, context, &event);
+ }
+};
+} // namespace
+
+//----------------------------------------------------------------------
+// Helper class: AutoIntervalUpdateBatcher
+
+// Stack-based helper class to set the mDeferIntervalUpdates flag on an
+// SMILTimedElement 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 SMILTimedElement is
+// destroyed.
+class MOZ_STACK_CLASS SMILTimedElement::AutoIntervalUpdateBatcher {
+ public:
+ explicit AutoIntervalUpdateBatcher(SMILTimedElement& 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:
+ SMILTimedElement& 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 SMILTimedElement::AutoIntervalUpdater {
+ public:
+ explicit AutoIntervalUpdater(SMILTimedElement& aTimedElement)
+ : mTimedElement(aTimedElement) {}
+
+ ~AutoIntervalUpdater() { mTimedElement.UpdateCurrentInterval(); }
+
+ private:
+ SMILTimedElement& mTimedElement;
+};
+
+//----------------------------------------------------------------------
+// Templated helper functions
+
+// Selectively remove elements from an array of type
+// nsTArray<RefPtr<SMILInstanceTime> > with O(n) performance.
+template <class TestFunctor>
+void SMILTimedElement::RemoveInstanceTimes(InstanceTimeList& aArray,
+ TestFunctor& aTest) {
+ InstanceTimeList newArray;
+ for (uint32_t i = 0; i < aArray.Length(); ++i) {
+ SMILInstanceTime* 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 = std::move(newArray);
+}
+
+//----------------------------------------------------------------------
+// Static members
+
+const nsAttrValue::EnumTable SMILTimedElement::sFillModeTable[] = {
+ {"remove", FILL_REMOVE}, {"freeze", FILL_FREEZE}, {nullptr, 0}};
+
+const nsAttrValue::EnumTable SMILTimedElement::sRestartModeTable[] = {
+ {"always", RESTART_ALWAYS},
+ {"whenNotActive", RESTART_WHENNOTACTIVE},
+ {"never", RESTART_NEVER},
+ {nullptr, 0}};
+
+const SMILMilestone SMILTimedElement::sMaxMilestone(
+ std::numeric_limits<SMILTime>::max(), false);
+
+// The thresholds at which point we start filtering intervals and instance times
+// indiscriminately.
+// See FilterIntervals and FilterInstanceTimes.
+const uint8_t SMILTimedElement::sMaxNumIntervals = 20;
+const uint8_t SMILTimedElement::sMaxNumInstanceTimes = 100;
+
+// Detect if we arrive in some sort of undetected recursive syncbase dependency
+// relationship
+const uint8_t SMILTimedElement::sMaxUpdateIntervalRecursionDepth = 20;
+
+//----------------------------------------------------------------------
+// Ctor, dtor
+
+SMILTimedElement::SMILTimedElement()
+ : 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();
+}
+
+SMILTimedElement::~SMILTimedElement() {
+ // 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 "
+ "SMILTimedElement disappears");
+ MOZ_ASSERT(!mDoDeferredUpdate,
+ "There should no longer be any pending updates when an "
+ "SMILTimedElement disappears");
+}
+
+void SMILTimedElement::SetAnimationElement(SVGAnimationElement* aElement) {
+ MOZ_ASSERT(aElement, "NULL owner element");
+ MOZ_ASSERT(!mAnimationElement, "Re-setting owner");
+ mAnimationElement = aElement;
+}
+
+SMILTimeContainer* SMILTimedElement::GetTimeContainer() {
+ return mAnimationElement ? mAnimationElement->GetTimeContainer() : nullptr;
+}
+
+dom::Element* SMILTimedElement::GetTargetElement() {
+ return mAnimationElement ? mAnimationElement->GetTargetElementContent()
+ : nullptr;
+}
+
+//----------------------------------------------------------------------
+// ElementTimeControl 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 SMILTimedElement::BeginElementAt(double aOffsetSeconds) {
+ SMILTimeContainer* container = GetTimeContainer();
+ if (!container) return NS_ERROR_FAILURE;
+
+ SMILTime currentTime = container->GetCurrentTimeAsSMILTime();
+ return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, true);
+}
+
+nsresult SMILTimedElement::EndElementAt(double aOffsetSeconds) {
+ SMILTimeContainer* container = GetTimeContainer();
+ if (!container) return NS_ERROR_FAILURE;
+
+ SMILTime currentTime = container->GetCurrentTimeAsSMILTime();
+ return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, false);
+}
+
+//----------------------------------------------------------------------
+// SVGAnimationElement methods
+
+SMILTimeValue SMILTimedElement::GetStartTime() const {
+ return mElementState == STATE_WAITING || mElementState == STATE_ACTIVE
+ ? mCurrentInterval->Begin()->Time()
+ : SMILTimeValue();
+}
+
+//----------------------------------------------------------------------
+// Hyperlinking support
+
+SMILTimeValue SMILTimedElement::GetHyperlinkTime() const {
+ SMILTimeValue hyperlinkTime; // Default ctor creates unresolved time
+
+ if (mElementState == STATE_ACTIVE) {
+ hyperlinkTime = mCurrentInterval->Begin()->Time();
+ } else if (!mBeginInstances.IsEmpty()) {
+ hyperlinkTime = mBeginInstances[0]->Time();
+ }
+
+ return hyperlinkTime;
+}
+
+//----------------------------------------------------------------------
+// SMILTimedElement
+
+void SMILTimedElement::AddInstanceTime(SMILInstanceTime* 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<SMILInstanceTime>* inserted =
+ instanceList.InsertElementSorted(aInstanceTime, InstanceTimeComparator());
+ if (!inserted) {
+ NS_WARNING("Insufficient memory to insert instance time");
+ return;
+ }
+
+ UpdateCurrentInterval();
+}
+
+void SMILTimedElement::UpdateInstanceTime(SMILInstanceTime* aInstanceTime,
+ SMILTimeValue& aUpdatedTime,
+ bool aIsBegin) {
+ MOZ_ASSERT(aInstanceTime, "Attempting to update null instance time");
+
+ // The reason we update the time here and not in the SMILTimeValueSpec 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 SMILTimedElement::RemoveInstanceTime(SMILInstanceTime* 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 SMILTimeValueSpec* aCreator)
+ : mCreator(aCreator) {}
+
+ bool operator()(SMILInstanceTime* 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 SMILTimeValueSpec* mCreator;
+};
+} // namespace
+
+void SMILTimedElement::RemoveInstanceTimesForCreator(
+ const SMILTimeValueSpec* aCreator, bool aIsBegin) {
+ MOZ_ASSERT(aCreator, "Creator not set");
+
+ InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances;
+ RemoveByCreator removeByCreator(aCreator);
+ RemoveInstanceTimes(instances, removeByCreator);
+
+ UpdateCurrentInterval();
+}
+
+void SMILTimedElement::SetTimeClient(SMILAnimationFunction* 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 SMILTimedElement::SampleAt(SMILTime aContainerTime) {
+ if (mIsDisabled) return;
+
+ // Milestones are cleared before a sample
+ mPrevRegisteredMilestone = sMaxMilestone;
+
+ DoSampleAt(aContainerTime, false);
+}
+
+void SMILTimedElement::SampleEndAt(SMILTime 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 SMILTimedElement::DoSampleAt(SMILTime 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(SMILTimeContainer::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;
+ SMILTimeValue 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: {
+ SMILInterval firstInterval;
+ mElementState =
+ GetNextInterval(nullptr, nullptr, nullptr, firstInterval)
+ ? STATE_WAITING
+ : STATE_POSTACTIVE;
+ stateChanged = true;
+ if (mElementState == STATE_WAITING) {
+ mCurrentInterval = MakeUnique<SMILInterval>(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) {
+ SMILInterval newInterval;
+ mElementState = GetNextInterval(mCurrentInterval.get(), 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(std::move(mCurrentInterval));
+ SampleFillValue();
+ if (mElementState == STATE_WAITING) {
+ mCurrentInterval = MakeUnique<SMILInterval>(newInterval);
+ }
+ // We are now in a consistent state to dispatch notifications
+ if (didApplyEarlyEnd) {
+ NotifyChangedInterval(
+ mOldIntervals[mOldIntervals.Length() - 1].get(), false, true);
+ }
+ if (mElementState == STATE_WAITING) {
+ NotifyNewInterval();
+ }
+ FilterHistory();
+ stateChanged = true;
+ } else if (mCurrentInterval->Begin()->Time() <= sampleTime) {
+ MOZ_ASSERT(!didApplyEarlyEnd, "We got an early end, but didn't end");
+ SMILTime beginTime = mCurrentInterval->Begin()->Time().GetMillis();
+ SMILTime 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() <= SMILTimeValue(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));
+ }
+ }
+ }
+ // Otherwise |sampleTime| is *before* the current interval. That
+ // normally doesn't happen but can happen if we get a stray milestone
+ // sample (e.g. if we registered a milestone with a time container that
+ // later got re-attached as a child of a more advanced time container).
+ // In that case we should just ignore the sample.
+ } 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 SMILTimedElement::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 SMILTimeValueSpec 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.get(), false, false);
+ }
+}
+
+namespace {
+bool RemoveNonDynamic(SMILInstanceTime* aInstanceTime) {
+ // Generally dynamically-generated instance times (DOM calls, event-based
+ // times) are not associated with their creator SMILTimeValueSpec 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 SMILTimedElement::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(SMILInstanceTime* aInstanceTime) { return true; }
+} // namespace
+
+bool SMILTimedElement::SetIsDisabled(bool aIsDisabled) {
+ if (mIsDisabled == aIsDisabled) return false;
+
+ if (aIsDisabled) {
+ mIsDisabled = true;
+ ClearTimingState(RemoveAll);
+ } else {
+ RebuildTimingState(RemoveAll);
+ mIsDisabled = false;
+ }
+ return true;
+}
+
+namespace {
+bool RemoveNonDOM(SMILInstanceTime* aInstanceTime) {
+ return !aInstanceTime->FromDOM() && !aInstanceTime->ShouldPreserve();
+}
+} // namespace
+
+bool SMILTimedElement::SetAttr(nsAtom* aAttribute, const nsAString& aValue,
+ nsAttrValue& aResult, Element& aContextElement,
+ nsresult* aParseResult) {
+ bool foundMatch = true;
+ nsresult parseResult = NS_OK;
+
+ if (aAttribute == nsGkAtoms::begin) {
+ parseResult = SetBeginSpec(aValue, aContextElement, RemoveNonDOM);
+ } else if (aAttribute == nsGkAtoms::dur) {
+ parseResult = SetSimpleDuration(aValue);
+ } else if (aAttribute == nsGkAtoms::end) {
+ parseResult = SetEndSpec(aValue, aContextElement, 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 SMILTimedElement::UnsetAttr(nsAtom* 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 SMILTimedElement::SetBeginSpec(const nsAString& aBeginSpec,
+ Element& aContextElement,
+ RemovalTestFunction aRemove) {
+ return SetBeginOrEndSpec(aBeginSpec, aContextElement, true /*isBegin*/,
+ aRemove);
+}
+
+void SMILTimedElement::UnsetBeginSpec(RemovalTestFunction aRemove) {
+ ClearSpecs(mBeginSpecs, mBeginInstances, aRemove);
+ UpdateCurrentInterval();
+}
+
+nsresult SMILTimedElement::SetEndSpec(const nsAString& aEndSpec,
+ Element& aContextElement,
+ RemovalTestFunction aRemove) {
+ return SetBeginOrEndSpec(aEndSpec, aContextElement, false /*!isBegin*/,
+ aRemove);
+}
+
+void SMILTimedElement::UnsetEndSpec(RemovalTestFunction aRemove) {
+ ClearSpecs(mEndSpecs, mEndInstances, aRemove);
+ UpdateCurrentInterval();
+}
+
+nsresult SMILTimedElement::SetSimpleDuration(const nsAString& aDurSpec) {
+ // Update the current interval before returning
+ AutoIntervalUpdater updater(*this);
+
+ SMILTimeValue duration;
+ const nsAString& dur = SMILParserUtils::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 (!SMILParserUtils::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 SMILTimedElement::UnsetSimpleDuration() {
+ mSimpleDur.SetIndefinite();
+ UpdateCurrentInterval();
+}
+
+nsresult SMILTimedElement::SetMin(const nsAString& aMinSpec) {
+ // Update the current interval before returning
+ AutoIntervalUpdater updater(*this);
+
+ SMILTimeValue duration;
+ const nsAString& min = SMILParserUtils::TrimWhitespace(aMinSpec);
+
+ if (min.EqualsLiteral("media")) {
+ duration.SetMillis(0L);
+ } else {
+ if (!SMILParserUtils::ParseClockValue(min, &duration)) {
+ mMin.SetMillis(0L);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ MOZ_ASSERT(duration.GetMillis() >= 0L, "Invalid duration");
+
+ mMin = duration;
+
+ return NS_OK;
+}
+
+void SMILTimedElement::UnsetMin() {
+ mMin.SetMillis(0L);
+ UpdateCurrentInterval();
+}
+
+nsresult SMILTimedElement::SetMax(const nsAString& aMaxSpec) {
+ // Update the current interval before returning
+ AutoIntervalUpdater updater(*this);
+
+ SMILTimeValue duration;
+ const nsAString& max = SMILParserUtils::TrimWhitespace(aMaxSpec);
+
+ if (max.EqualsLiteral("media") || max.EqualsLiteral("indefinite")) {
+ duration.SetIndefinite();
+ } else {
+ if (!SMILParserUtils::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 SMILTimedElement::UnsetMax() {
+ mMax.SetIndefinite();
+ UpdateCurrentInterval();
+}
+
+nsresult SMILTimedElement::SetRestart(const nsAString& aRestartSpec) {
+ nsAttrValue temp;
+ bool parseResult = temp.ParseEnumValue(aRestartSpec, sRestartModeTable, true);
+ mRestartMode =
+ parseResult ? SMILRestartMode(temp.GetEnumValue()) : RESTART_ALWAYS;
+ UpdateCurrentInterval();
+ return parseResult ? NS_OK : NS_ERROR_FAILURE;
+}
+
+void SMILTimedElement::UnsetRestart() {
+ mRestartMode = RESTART_ALWAYS;
+ UpdateCurrentInterval();
+}
+
+nsresult SMILTimedElement::SetRepeatCount(const nsAString& aRepeatCountSpec) {
+ // Update the current interval before returning
+ AutoIntervalUpdater updater(*this);
+
+ SMILRepeatCount newRepeatCount;
+
+ if (SMILParserUtils::ParseRepeatCount(aRepeatCountSpec, newRepeatCount)) {
+ mRepeatCount = newRepeatCount;
+ return NS_OK;
+ }
+ mRepeatCount.Unset();
+ return NS_ERROR_FAILURE;
+}
+
+void SMILTimedElement::UnsetRepeatCount() {
+ mRepeatCount.Unset();
+ UpdateCurrentInterval();
+}
+
+nsresult SMILTimedElement::SetRepeatDur(const nsAString& aRepeatDurSpec) {
+ // Update the current interval before returning
+ AutoIntervalUpdater updater(*this);
+
+ SMILTimeValue duration;
+
+ const nsAString& repeatDur = SMILParserUtils::TrimWhitespace(aRepeatDurSpec);
+
+ if (repeatDur.EqualsLiteral("indefinite")) {
+ duration.SetIndefinite();
+ } else {
+ if (!SMILParserUtils::ParseClockValue(repeatDur, &duration)) {
+ mRepeatDur.SetUnresolved();
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ mRepeatDur = duration;
+
+ return NS_OK;
+}
+
+void SMILTimedElement::UnsetRepeatDur() {
+ mRepeatDur.SetUnresolved();
+ UpdateCurrentInterval();
+}
+
+nsresult SMILTimedElement::SetFillMode(const nsAString& aFillModeSpec) {
+ uint16_t previousFillMode = mFillMode;
+
+ nsAttrValue temp;
+ bool parseResult = temp.ParseEnumValue(aFillModeSpec, sFillModeTable, true);
+ mFillMode = parseResult ? SMILFillMode(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 SMILTimedElement::UnsetFillMode() {
+ uint16_t previousFillMode = mFillMode;
+ mFillMode = FILL_REMOVE;
+ if (previousFillMode == FILL_FREEZE && HasClientInFillRange()) {
+ mClient->Inactivate(false);
+ }
+}
+
+void SMILTimedElement::AddDependent(SMILTimeValueSpec& aDependent) {
+ // There's probably no harm in attempting to register a dependent
+ // SMILTimeValueSpec twice, but we're not expecting it to happen.
+ MOZ_ASSERT(!mTimeDependents.GetEntry(&aDependent),
+ "SMILTimeValueSpec 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 SMILTimedElement::RemoveDependent(SMILTimeValueSpec& aDependent) {
+ mTimeDependents.RemoveEntry(&aDependent);
+}
+
+bool SMILTimedElement::IsTimeDependent(const SMILTimedElement& aOther) const {
+ const SMILInstanceTime* thisBegin = GetEffectiveBeginInstance();
+ const SMILInstanceTime* otherBegin = aOther.GetEffectiveBeginInstance();
+
+ if (!thisBegin || !otherBegin) return false;
+
+ return thisBegin->IsDependentOn(*otherBegin);
+}
+
+void SMILTimedElement::BindToTree(Element& aContextElement) {
+ // 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(aContextElement);
+ }
+
+ count = mEndSpecs.Length();
+ for (uint32_t j = 0; j < count; ++j) {
+ mEndSpecs[j]->ResolveReferences(aContextElement);
+ }
+ }
+
+ RegisterMilestone();
+}
+
+void SMILTimedElement::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 SMILTimedElement::Traverse(nsCycleCollectionTraversalCallback* aCallback) {
+ uint32_t count = mBeginSpecs.Length();
+ for (uint32_t i = 0; i < count; ++i) {
+ SMILTimeValueSpec* beginSpec = mBeginSpecs[i].get();
+ MOZ_ASSERT(beginSpec, "null SMILTimeValueSpec in list of begin specs");
+ beginSpec->Traverse(aCallback);
+ }
+
+ count = mEndSpecs.Length();
+ for (uint32_t j = 0; j < count; ++j) {
+ SMILTimeValueSpec* endSpec = mEndSpecs[j].get();
+ MOZ_ASSERT(endSpec, "null SMILTimeValueSpec in list of end specs");
+ endSpec->Traverse(aCallback);
+ }
+}
+
+void SMILTimedElement::Unlink() {
+ AutoIntervalUpdateBatcher updateBatcher(*this);
+
+ // Remove dependencies on other elements
+ uint32_t count = mBeginSpecs.Length();
+ for (uint32_t i = 0; i < count; ++i) {
+ SMILTimeValueSpec* beginSpec = mBeginSpecs[i].get();
+ MOZ_ASSERT(beginSpec, "null SMILTimeValueSpec in list of begin specs");
+ beginSpec->Unlink();
+ }
+
+ count = mEndSpecs.Length();
+ for (uint32_t j = 0; j < count; ++j) {
+ SMILTimeValueSpec* endSpec = mEndSpecs[j].get();
+ MOZ_ASSERT(endSpec, "null SMILTimeValueSpec in list of end specs");
+ endSpec->Unlink();
+ }
+
+ ClearIntervals();
+
+ // Make sure we don't notify other elements of new intervals
+ mTimeDependents.Clear();
+}
+
+//----------------------------------------------------------------------
+// Implementation helpers
+
+nsresult SMILTimedElement::SetBeginOrEndSpec(const nsAString& aSpec,
+ Element& aContextElement,
+ 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;
+ }
+
+ bool hadFailure = false;
+ while (tokenizer.hasMoreTokens()) {
+ auto spec = MakeUnique<SMILTimeValueSpec>(*this, aIsBegin);
+ nsresult rv = spec->SetSpec(tokenizer.nextToken(), aContextElement);
+ if (NS_SUCCEEDED(rv)) {
+ timeSpecsList.AppendElement(std::move(spec));
+ } else {
+ hadFailure = true;
+ }
+ }
+
+ // The return value from this function is only used to determine if we should
+ // print a console message or not, so we return failure if we had one or more
+ // failures but we don't need to differentiate between different types of
+ // failures or the number of failures.
+ return hadFailure ? NS_ERROR_FAILURE : NS_OK;
+}
+
+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(SMILTimedElement::RemovalTestFunction aFunction)
+ : mFunction(aFunction) {}
+ bool operator()(SMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) {
+ return mFunction(aInstanceTime);
+ }
+
+ private:
+ SMILTimedElement::RemovalTestFunction mFunction;
+};
+} // namespace
+
+void SMILTimedElement::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 SMILTimedElement::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 SMILTimedElement::ApplyEarlyEnd(const SMILTimeValue& 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) {
+ SMILInstanceTime* 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<SMILInstanceTime> newEarlyEnd =
+ new SMILInstanceTime(earlyEnd->Time());
+ mCurrentInterval->SetEnd(*newEarlyEnd);
+ } else {
+ mCurrentInterval->SetEnd(*earlyEnd);
+ }
+ updated = true;
+ }
+ }
+ return updated;
+}
+
+namespace {
+class MOZ_STACK_CLASS RemoveReset {
+ public:
+ explicit RemoveReset(const SMILInstanceTime* aCurrentIntervalBegin)
+ : mCurrentIntervalBegin(aCurrentIntervalBegin) {}
+ bool operator()(SMILInstanceTime* 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 SMILInstanceTime* mCurrentIntervalBegin;
+};
+} // namespace
+
+void SMILTimedElement::Reset() {
+ RemoveReset resetBegin(mCurrentInterval ? mCurrentInterval->Begin()
+ : nullptr);
+ RemoveInstanceTimes(mBeginInstances, resetBegin);
+
+ RemoveReset resetEnd(nullptr);
+ RemoveInstanceTimes(mEndInstances, resetEnd);
+}
+
+void SMILTimedElement::ClearTimingState(RemovalTestFunction aRemove) {
+ mElementState = STATE_STARTUP;
+ ClearIntervals();
+
+ UnsetBeginSpec(aRemove);
+ UnsetEndSpec(aRemove);
+
+ if (mClient) {
+ mClient->Inactivate(false);
+ }
+}
+
+void SMILTimedElement::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->HasAttr(nsGkAtoms::begin)) {
+ nsAutoString attValue;
+ mAnimationElement->GetAttr(nsGkAtoms::begin, attValue);
+ SetBeginSpec(attValue, *mAnimationElement, aRemove);
+ }
+
+ if (mAnimationElement->HasAttr(nsGkAtoms::end)) {
+ nsAutoString attValue;
+ mAnimationElement->GetAttr(nsGkAtoms::end, attValue);
+ SetEndSpec(attValue, *mAnimationElement, aRemove);
+ }
+
+ mPrevRegisteredMilestone = sMaxMilestone;
+ RegisterMilestone();
+}
+
+void SMILTimedElement::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 SMILTimedElement::UnpreserveInstanceTimes(InstanceTimeList& aList) {
+ const SMILInterval* prevInterval = GetPreviousInterval();
+ const SMILInstanceTime* cutoff = mCurrentInterval ? mCurrentInterval->Begin()
+ : prevInterval ? prevInterval->Begin()
+ : nullptr;
+ uint32_t count = aList.Length();
+ for (uint32_t i = 0; i < count; ++i) {
+ SMILInstanceTime* instance = aList[i].get();
+ if (!cutoff || cutoff->Time().CompareTo(instance->Time()) < 0) {
+ instance->UnmarkShouldPreserve();
+ }
+ }
+}
+
+void SMILTimedElement::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 SMILTimedElement::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) {
+ SMILInterval* 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(std::move(mOldIntervals[i]));
+ }
+ }
+ mOldIntervals = std::move(filteredList);
+}
+
+namespace {
+class MOZ_STACK_CLASS RemoveFiltered {
+ public:
+ explicit RemoveFiltered(SMILTimeValue aCutoff) : mCutoff(aCutoff) {}
+ bool operator()(SMILInstanceTime* 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:
+ SMILTimeValue mCutoff;
+};
+
+class MOZ_STACK_CLASS RemoveBelowThreshold {
+ public:
+ RemoveBelowThreshold(uint32_t aThreshold,
+ nsTArray<const SMILInstanceTime*>& aTimesToKeep)
+ : mThreshold(aThreshold), mTimesToKeep(aTimesToKeep) {}
+ bool operator()(SMILInstanceTime* aInstanceTime, uint32_t aIndex) {
+ return aIndex < mThreshold && !mTimesToKeep.Contains(aInstanceTime);
+ }
+
+ private:
+ uint32_t mThreshold;
+ nsTArray<const SMILInstanceTime*>& mTimesToKeep;
+};
+} // namespace
+
+void SMILTimedElement::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 SMILInstanceTime*> timesToKeep;
+ if (mCurrentInterval) {
+ timesToKeep.AppendElement(mCurrentInterval->Begin());
+ }
+ const SMILInterval* 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 SMILTimedElement::GetNextInterval(const SMILInterval* aPrevInterval,
+ const SMILInterval* aReplacedInterval,
+ const SMILInstanceTime* aFixedBeginTime,
+ SMILInterval& aResult) const {
+ MOZ_ASSERT(!aFixedBeginTime || aFixedBeginTime->Time().IsDefinite(),
+ "Unresolved or indefinite begin time given for interval start");
+ static const SMILTimeValue zeroTime(0L);
+
+ if (mRestartMode == RESTART_NEVER && aPrevInterval) return false;
+
+ // Calc starting point
+ SMILTimeValue beginAfter;
+ bool prevIntervalWasZeroDur = false;
+ if (aPrevInterval) {
+ beginAfter = aPrevInterval->End()->Time();
+ prevIntervalWasZeroDur =
+ aPrevInterval->End()->Time() == aPrevInterval->Begin()->Time();
+ } else {
+ beginAfter.SetMillis(INT64_MIN);
+ }
+
+ RefPtr<SMILInstanceTime> tempBegin;
+ RefPtr<SMILInstanceTime> tempEnd;
+
+ while (true) {
+ // Calculate begin time
+ if (aFixedBeginTime) {
+ if (aFixedBeginTime->Time() < beginAfter) {
+ return false;
+ }
+ // our ref-counting is not const-correct
+ tempBegin = const_cast<SMILInstanceTime*>(aFixedBeginTime);
+ } else if ((!mAnimationElement ||
+ !mAnimationElement->HasAttr(nsGkAtoms::begin)) &&
+ beginAfter <= zeroTime) {
+ tempBegin = new SMILInstanceTime(SMILTimeValue(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
+ }
+ }
+
+ SMILTimeValue intervalEnd = tempEnd ? tempEnd->Time() : SMILTimeValue();
+ SMILTimeValue activeEnd = CalcActiveEnd(tempBegin->Time(), intervalEnd);
+
+ if (!tempEnd || intervalEnd != activeEnd) {
+ tempEnd = new SMILInstanceTime(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) {
+ 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();
+ }
+ MOZ_ASSERT_UNREACHABLE("Hmm... we really shouldn't be here");
+
+ return false;
+}
+
+SMILInstanceTime* SMILTimedElement::GetNextGreater(
+ const InstanceTimeList& aList, const SMILTimeValue& aBase,
+ int32_t& aPosition) const {
+ SMILInstanceTime* result = nullptr;
+ while ((result = GetNextGreaterOrEqual(aList, aBase, aPosition)) &&
+ result->Time() == aBase) {
+ }
+ return result;
+}
+
+SMILInstanceTime* SMILTimedElement::GetNextGreaterOrEqual(
+ const InstanceTimeList& aList, const SMILTimeValue& aBase,
+ int32_t& aPosition) const {
+ SMILInstanceTime* result = nullptr;
+ int32_t count = aList.Length();
+
+ for (; aPosition < count && !result; ++aPosition) {
+ SMILInstanceTime* 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
+ */
+SMILTimeValue SMILTimedElement::CalcActiveEnd(const SMILTimeValue& aBegin,
+ const SMILTimeValue& aEnd) const {
+ SMILTimeValue 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()) {
+ SMILTime 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()) {
+ SMILTime activeEnd = result.GetMillis() + aBegin.GetMillis();
+ result.SetMillis(activeEnd);
+ }
+
+ return result;
+}
+
+SMILTimeValue SMILTimedElement::GetRepeatDuration() const {
+ SMILTimeValue multipliedDuration;
+ if (mRepeatCount.IsDefinite() && mSimpleDur.IsDefinite()) {
+ if (mRepeatCount * double(mSimpleDur.GetMillis()) <
+ double(std::numeric_limits<SMILTime>::max())) {
+ multipliedDuration.SetMillis(
+ SMILTime(mRepeatCount * mSimpleDur.GetMillis()));
+ }
+ } else {
+ multipliedDuration.SetIndefinite();
+ }
+
+ SMILTimeValue repeatDuration;
+
+ if (mRepeatDur.IsResolved()) {
+ repeatDuration = std::min(multipliedDuration, mRepeatDur);
+ } else if (mRepeatCount.IsSet()) {
+ repeatDuration = multipliedDuration;
+ } else {
+ repeatDuration = mSimpleDur;
+ }
+
+ return repeatDuration;
+}
+
+SMILTimeValue SMILTimedElement::ApplyMinAndMax(
+ const SMILTimeValue& aDuration) const {
+ if (!aDuration.IsResolved()) {
+ return aDuration;
+ }
+
+ if (mMax < mMin) {
+ return aDuration;
+ }
+
+ SMILTimeValue result;
+
+ if (aDuration > mMax) {
+ result = mMax;
+ } else if (aDuration < mMin) {
+ result = mMin;
+ } else {
+ result = aDuration;
+ }
+
+ return result;
+}
+
+SMILTime SMILTimedElement::ActiveTimeToSimpleTime(SMILTime aActiveTime,
+ uint32_t& aRepeatIteration) {
+ SMILTime 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.'
+//
+SMILInstanceTime* SMILTimedElement::CheckForEarlyEnd(
+ const SMILTimeValue& 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;
+ SMILInstanceTime* 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 SMILTimedElement::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 SMILInstanceTime* beginTime =
+ mElementState == STATE_ACTIVE ? mCurrentInterval->Begin() : nullptr;
+ SMILInterval updatedInterval;
+ if (GetNextInterval(GetPreviousInterval(), mCurrentInterval.get(), beginTime,
+ updatedInterval)) {
+ if (mElementState == STATE_POSTACTIVE) {
+ MOZ_ASSERT(!mCurrentInterval,
+ "In postactive state but the interval has been set");
+ mCurrentInterval = MakeUnique<SMILInterval>(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.get(), 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.get(), 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 SMILTimedElement::SampleSimpleTime(SMILTime aActiveTime) {
+ if (mClient) {
+ uint32_t repeatIteration;
+ SMILTime simpleTime = ActiveTimeToSimpleTime(aActiveTime, repeatIteration);
+ mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration);
+ }
+}
+
+void SMILTimedElement::SampleFillValue() {
+ if (mFillMode != FILL_FREEZE || !mClient) return;
+
+ SMILTime activeTime;
+
+ if (mElementState == STATE_WAITING || mElementState == STATE_POSTACTIVE) {
+ const SMILInterval* 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.
+ SMILTimeValue 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;
+ SMILTime simpleTime = ActiveTimeToSimpleTime(activeTime, repeatIteration);
+
+ if (simpleTime == 0L && repeatIteration) {
+ mClient->SampleLastValue(--repeatIteration);
+ } else {
+ mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration);
+ }
+}
+
+nsresult SMILTimedElement::AddInstanceTimeFromCurrentTime(SMILTime aCurrentTime,
+ double aOffsetSeconds,
+ bool aIsBegin) {
+ double offset = NS_round(aOffsetSeconds * PR_MSEC_PER_SEC);
+
+ // Check we won't overflow the range of SMILTime
+ if (aCurrentTime + offset > double(std::numeric_limits<SMILTime>::max()))
+ return NS_ERROR_ILLEGAL_VALUE;
+
+ SMILTimeValue timeVal(aCurrentTime + int64_t(offset));
+
+ RefPtr<SMILInstanceTime> instanceTime =
+ new SMILInstanceTime(timeVal, SMILInstanceTime::SOURCE_DOM);
+
+ AddInstanceTime(instanceTime, aIsBegin);
+
+ return NS_OK;
+}
+
+void SMILTimedElement::RegisterMilestone() {
+ SMILTimeContainer* container = GetTimeContainer();
+ if (!container) return;
+ MOZ_ASSERT(mAnimationElement,
+ "Got a time container without an owning animation element");
+
+ SMILMilestone nextMilestone;
+ if (!GetNextMilestone(nextMilestone)) return;
+
+ // This method is called every time we might possibly have updated our
+ // current interval, but since SMILTimeContainer 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 SMILTimedElement::GetNextMilestone(SMILMilestone& 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
+ SMILTimeValue nextRepeat;
+ if (mSeekState == SEEK_NOT_SEEKING && mSimpleDur.IsDefinite()) {
+ SMILTime nextRepeatActiveTime =
+ (mCurrentRepeatIteration + 1) * mSimpleDur.GetMillis();
+ // Check that the repeat fits within the repeat duration
+ if (SMILTimeValue(nextRepeatActiveTime) < GetRepeatDuration()) {
+ nextRepeat.SetMillis(mCurrentInterval->Begin()->Time().GetMillis() +
+ nextRepeatActiveTime);
+ }
+ }
+ SMILTimeValue nextMilestone =
+ std::min(mCurrentInterval->End()->Time(), nextRepeat);
+
+ // Check for an early end before that time
+ SMILInstanceTime* 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 SMILTimedElement::NotifyNewInterval() {
+ MOZ_ASSERT(mCurrentInterval,
+ "Attempting to notify dependents of a new interval but the "
+ "interval is not set");
+
+ SMILTimeContainer* container = GetTimeContainer();
+ if (container) {
+ container->SyncPauseTime();
+ }
+
+ for (auto iter = mTimeDependents.Iter(); !iter.Done(); iter.Next()) {
+ SMILInterval* interval = mCurrentInterval.get();
+ // 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;
+ }
+ SMILTimeValueSpec* spec = iter.Get()->GetKey();
+ spec->HandleNewInterval(*interval, container);
+ }
+}
+
+void SMILTimedElement::NotifyChangedInterval(SMILInterval* aInterval,
+ bool aBeginObjectChanged,
+ bool aEndObjectChanged) {
+ MOZ_ASSERT(aInterval, "Null interval for change notification");
+
+ SMILTimeContainer* 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 SMILTimedElement::FireTimeEventAsync(EventMessage aMsg, int32_t aDetail) {
+ if (!mAnimationElement) return;
+
+ nsCOMPtr<nsIRunnable> event =
+ new AsyncTimeEventRunner(mAnimationElement, aMsg, aDetail);
+ mAnimationElement->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget());
+}
+
+const SMILInstanceTime* SMILTimedElement::GetEffectiveBeginInstance() const {
+ switch (mElementState) {
+ case STATE_STARTUP:
+ return nullptr;
+
+ case STATE_ACTIVE:
+ return mCurrentInterval->Begin();
+
+ case STATE_WAITING:
+ case STATE_POSTACTIVE: {
+ const SMILInterval* prevInterval = GetPreviousInterval();
+ return prevInterval ? prevInterval->Begin() : nullptr;
+ }
+ }
+ MOZ_CRASH("Invalid element state");
+}
+
+const SMILInterval* SMILTimedElement::GetPreviousInterval() const {
+ return mOldIntervals.IsEmpty()
+ ? nullptr
+ : mOldIntervals[mOldIntervals.Length() - 1].get();
+}
+
+bool SMILTimedElement::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 SMILTimedElement::EndHasEventConditions() const {
+ for (uint32_t i = 0; i < mEndSpecs.Length(); ++i) {
+ if (mEndSpecs[i]->IsEventBased()) return true;
+ }
+ return false;
+}
+
+bool SMILTimedElement::AreEndTimesDependentOn(
+ const SMILInstanceTime* 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;
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILTimedElement.h b/dom/smil/SMILTimedElement.h
new file mode 100644
index 0000000000..fc2792d1dc
--- /dev/null
+++ b/dom/smil/SMILTimedElement.h
@@ -0,0 +1,649 @@
+/* -*- 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 DOM_SMIL_SMILTIMEDELEMENT_H_
+#define DOM_SMIL_SMILTIMEDELEMENT_H_
+
+#include <utility>
+
+#include "mozilla/EventForwards.h"
+#include "mozilla/SMILInstanceTime.h"
+#include "mozilla/SMILInterval.h"
+#include "mozilla/SMILMilestone.h"
+#include "mozilla/SMILRepeatCount.h"
+#include "mozilla/SMILTimeValueSpec.h"
+#include "mozilla/SMILTypes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsAttrValue.h"
+#include "nsHashKeys.h"
+#include "nsTArray.h"
+#include "nsTHashtable.h"
+
+class nsAtom;
+
+namespace mozilla {
+
+class SMILAnimationFunction;
+class SMILTimeContainer;
+class SMILTimeValue;
+
+namespace dom {
+class SVGAnimationElement;
+} // namespace dom
+
+//----------------------------------------------------------------------
+// SMILTimedElement
+
+class SMILTimedElement {
+ public:
+ SMILTimedElement();
+ ~SMILTimedElement();
+
+ using Element = dom::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.
+ */
+ SMILTimeContainer* GetTimeContainer();
+
+ /*
+ * Returns the element targeted by the animation element. Needed for
+ * registering event listeners against the appropriate element.
+ */
+ Element* GetTargetElement();
+
+ /**
+ * Methods for supporting the ElementTimeControl 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 SVGAnimationElement 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.
+ */
+ SMILTimeValue GetStartTime() const;
+
+ /**
+ * Returns the simple duration of this element.
+ *
+ * @return the simple duration in milliseconds or INDEFINITE.
+ */
+ SMILTimeValue 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.
+ */
+ SMILTimeValue 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 SMILTimeValueSpec.
+ *
+ * @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(SMILInstanceTime* aInstanceTime, bool aIsBegin);
+
+ /**
+ * Requests this element update the given instance time.
+ *
+ * This method is typically called by a child SMILTimeValueSpec.
+ *
+ * @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(SMILInstanceTime* aInstanceTime,
+ SMILTimeValue& aUpdatedTime, bool aIsBegin);
+
+ /**
+ * Removes an instance time object from this element's list of instance times.
+ *
+ * This method is typically called by a child SMILTimeValueSpec.
+ *
+ * @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(SMILInstanceTime* aInstanceTime, bool aIsBegin);
+
+ /**
+ * Removes all the instance times associated with the given
+ * SMILTimeValueSpec object. Used when an ID assignment changes and hence
+ * all the previously associated instance times become invalid.
+ *
+ * @param aCreator The SMILTimeValueSpec object whose created
+ * SMILInstanceTime'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 SMILTimeValueSpec* aCreator,
+ 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(SMILAnimationFunction* 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(SMILTime 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(SMILTime 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 aContextElement 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(nsAtom* aAttribute, const nsAString& aValue,
+ nsAttrValue& aResult, Element& aContextElement,
+ 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(nsAtom* 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 SMILTimeValueSpec 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(SMILTimeValueSpec& aDependent);
+
+ /**
+ * Removes a syncbase dependency from the list of dependents that are notified
+ * when the current interval is modified.
+ *
+ * @param aDependent The SMILTimeValueSpec object to unregister.
+ */
+ void RemoveDependent(SMILTimeValueSpec& 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 SMILTimedElement& 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 aContextElement The element which provides the necessary context for
+ * resolving references. This is typically the element
+ * in the host language that owns this timed element.
+ */
+ void BindToTree(Element& aContextElement);
+
+ /**
+ * Called when the target of the animation has changed so that event
+ * registrations can be updated.
+ */
+ void HandleTargetElementChange(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();
+
+ using RemovalTestFunction = bool (*)(SMILInstanceTime* aInstance);
+
+ protected:
+ // Typedefs
+ using TimeValueSpecList = nsTArray<UniquePtr<SMILTimeValueSpec>>;
+ using InstanceTimeList = nsTArray<RefPtr<SMILInstanceTime>>;
+ using IntervalList = nsTArray<UniquePtr<SMILInterval>>;
+ using TimeValueSpecPtrKey = nsPtrHashKey<SMILTimeValueSpec>;
+ using TimeValueSpecHashSet = nsTHashtable<TimeValueSpecPtrKey>;
+
+ // Helper classes
+ class InstanceTimeComparator {
+ public:
+ bool Equals(const SMILInstanceTime* aElem1,
+ const SMILInstanceTime* aElem2) const;
+ bool LessThan(const SMILInstanceTime* aElem1,
+ const SMILInstanceTime* aElem2) const;
+ };
+
+ // Templated helper functions
+ template <class TestFunctor>
+ void RemoveInstanceTimes(InstanceTimeList& aArray, TestFunctor& aTest);
+
+ //
+ // Implementation helpers
+ //
+
+ nsresult SetBeginSpec(const nsAString& aBeginSpec, Element& aContextElement,
+ RemovalTestFunction aRemove);
+ nsresult SetEndSpec(const nsAString& aEndSpec, Element& aContextElement,
+ 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& aContextElement,
+ bool aIsBegin, RemovalTestFunction aRemove);
+ void ClearSpecs(TimeValueSpecList& aSpecs, InstanceTimeList& aInstances,
+ RemovalTestFunction aRemove);
+ void ClearIntervals();
+ void DoSampleAt(SMILTime 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 SMILTimeValue& 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 SMILIntervals and SMILTimeInstanceTimes)
+ * and discard items that are no longer needed or exceed some threshold of
+ * accumulated state.
+ */
+ void FilterHistory();
+
+ // Helper functions for FilterHistory to clear old SMILIntervals and
+ // SMILInstanceTimes 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 SMILInterval* aPrevInterval,
+ const SMILInterval* aReplacedInterval,
+ const SMILInstanceTime* aFixedBeginTime,
+ SMILInterval& aResult) const;
+ SMILInstanceTime* GetNextGreater(const InstanceTimeList& aList,
+ const SMILTimeValue& aBase,
+ int32_t& aPosition) const;
+ SMILInstanceTime* GetNextGreaterOrEqual(const InstanceTimeList& aList,
+ const SMILTimeValue& aBase,
+ int32_t& aPosition) const;
+ SMILTimeValue CalcActiveEnd(const SMILTimeValue& aBegin,
+ const SMILTimeValue& aEnd) const;
+ SMILTimeValue GetRepeatDuration() const;
+ SMILTimeValue ApplyMinAndMax(const SMILTimeValue& aDuration) const;
+ SMILTime ActiveTimeToSimpleTime(SMILTime aActiveTime,
+ uint32_t& aRepeatIteration);
+ SMILInstanceTime* CheckForEarlyEnd(const SMILTimeValue& aContainerTime) const;
+ void UpdateCurrentInterval(bool aForceChangeNotice = false);
+ void SampleSimpleTime(SMILTime aActiveTime);
+ void SampleFillValue();
+ nsresult AddInstanceTimeFromCurrentTime(SMILTime aCurrentTime,
+ double aOffsetSeconds, bool aIsBegin);
+ void RegisterMilestone();
+ bool GetNextMilestone(SMILMilestone& 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(SMILInterval* aInterval, bool aBeginObjectChanged,
+ bool aEndObjectChanged);
+
+ void FireTimeEventAsync(EventMessage aMsg, int32_t aDetail);
+ const SMILInstanceTime* GetEffectiveBeginInstance() const;
+ const SMILInterval* GetPreviousInterval() const;
+ bool HasPlayed() const { return !mOldIntervals.IsEmpty(); }
+ bool HasClientInFillRange() const;
+ bool EndHasEventConditions() const;
+ bool AreEndTimesDependentOn(const SMILInstanceTime* 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.)
+ auto interval = std::move(mCurrentInterval);
+ interval->Unlink();
+ }
+ }
+
+ //
+ // Members
+ //
+ mozilla::dom::SVGAnimationElement* mAnimationElement; // [weak] won't outlive
+ // owner
+ TimeValueSpecList mBeginSpecs; // [strong]
+ TimeValueSpecList mEndSpecs; // [strong]
+
+ SMILTimeValue mSimpleDur;
+
+ SMILRepeatCount mRepeatCount;
+ SMILTimeValue mRepeatDur;
+
+ SMILTimeValue mMin;
+ SMILTimeValue mMax;
+
+ enum SMILFillMode : uint8_t { FILL_REMOVE, FILL_FREEZE };
+ SMILFillMode mFillMode;
+ static const nsAttrValue::EnumTable sFillModeTable[];
+
+ enum SMILRestartMode : uint8_t {
+ RESTART_ALWAYS,
+ RESTART_WHENNOTACTIVE,
+ RESTART_NEVER
+ };
+ SMILRestartMode mRestartMode;
+ static const nsAttrValue::EnumTable sRestartModeTable[];
+
+ InstanceTimeList mBeginInstances;
+ InstanceTimeList mEndInstances;
+ uint32_t mInstanceSerialIndex;
+
+ SMILAnimationFunction* mClient;
+ UniquePtr<SMILInterval> mCurrentInterval;
+ IntervalList mOldIntervals;
+ uint32_t mCurrentRepeatIteration;
+ SMILMilestone mPrevRegisteredMilestone;
+ static const SMILMilestone 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 SMILTimeValueSpec 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 SMILElementState {
+ STATE_STARTUP,
+ STATE_WAITING,
+ STATE_ACTIVE,
+ STATE_POSTACTIVE
+ };
+ SMILElementState mElementState;
+
+ enum SMILSeekState {
+ SEEK_NOT_SEEKING,
+ SEEK_FORWARD_FROM_ACTIVE,
+ SEEK_FORWARD_FROM_INACTIVE,
+ SEEK_BACKWARD_FROM_ACTIVE,
+ SEEK_BACKWARD_FROM_INACTIVE
+ };
+ SMILSeekState 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(SMILTimedElement& aField) {
+ aField.Unlink();
+}
+
+inline void ImplCycleCollectionTraverse(
+ nsCycleCollectionTraversalCallback& aCallback, SMILTimedElement& aField,
+ const char* aName, uint32_t aFlags = 0) {
+ aField.Traverse(&aCallback);
+}
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILTIMEDELEMENT_H_
diff --git a/dom/smil/SMILType.h b/dom/smil/SMILType.h
new file mode 100644
index 0000000000..5aef1e7623
--- /dev/null
+++ b/dom/smil/SMILType.h
@@ -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/. */
+
+#ifndef DOM_SMIL_SMILTYPE_H_
+#define DOM_SMIL_SMILTYPE_H_
+
+#include "mozilla/Attributes.h"
+#include "nscore.h"
+
+namespace mozilla {
+
+class SMILValue;
+
+//////////////////////////////////////////////////////////////////////////////
+// SMILType: 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 a SMILValue 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 SMILValue. This is so that sizeof(SMILValue) is the same
+// for all value types, allowing us to have a type-agnostic nsTArray of
+// SMILValue objects (actual objects, not pointers). It also allows most
+// SMILValues (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 SMILType {
+ /**
+ * Only give the SMILValue class access to this interface.
+ */
+ friend class SMILValue;
+
+ 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(SMILValue& aValue) const = 0;
+
+ /**
+ * Destroys any data associated with a value of this type.
+ *
+ * @pre aValue.mType == this
+ * @post aValue.IsNull()
+ */
+ virtual void Destroy(SMILValue& 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(SMILValue& aDest, const SMILValue& aSrc) const = 0;
+
+ /**
+ * Test two SMILValue objects (of this SMILType) 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 SMILValue& aLeft,
+ const SMILValue& 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(SMILValue& aDest, const SMILValue& 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(SMILValue& aDest,
+ const SMILValue& 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 SMILValue& aFrom, const SMILValue& 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 SMILValue& aStartVal,
+ const SMILValue& aEndVal, double aUnitDistance,
+ SMILValue& aResult) const = 0;
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILTYPE_H_
diff --git a/dom/smil/SMILTypes.h b/dom/smil/SMILTypes.h
new file mode 100644
index 0000000000..6391872f9e
--- /dev/null
+++ b/dom/smil/SMILTypes.h
@@ -0,0 +1,30 @@
+/* -*- 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 DOM_SMIL_SMILTYPES_H_
+#define DOM_SMIL_SMILTYPES_H_
+
+#include <stdint.h>
+
+namespace mozilla {
+
+// 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 documentation in SMILTimeValue.h
+//
+using SMILTime = int64_t;
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILTYPES_H_
diff --git a/dom/smil/SMILValue.cpp b/dom/smil/SMILValue.cpp
new file mode 100644
index 0000000000..4942e3b8b6
--- /dev/null
+++ b/dom/smil/SMILValue.cpp
@@ -0,0 +1,142 @@
+/* -*- 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 "SMILValue.h"
+
+#include "nsDebug.h"
+#include <string.h>
+
+namespace mozilla {
+
+//----------------------------------------------------------------------
+// Public methods
+
+SMILValue::SMILValue(const SMILType* aType) : mType(SMILNullType::Singleton()) {
+ mU.mBool = false;
+ if (!aType) {
+ NS_ERROR("Trying to construct SMILValue with null mType pointer");
+ return;
+ }
+
+ InitAndCheckPostcondition(aType);
+}
+
+SMILValue::SMILValue(const SMILValue& aVal) : mType(SMILNullType::Singleton()) {
+ InitAndCheckPostcondition(aVal.mType);
+ mType->Assign(*this, aVal);
+}
+
+const SMILValue& SMILValue::operator=(const SMILValue& aVal) {
+ if (&aVal == this) return *this;
+
+ if (mType != aVal.mType) {
+ DestroyAndReinit(aVal.mType);
+ }
+
+ mType->Assign(*this, aVal);
+
+ return *this;
+}
+
+// Move constructor / reassignment operator:
+SMILValue::SMILValue(SMILValue&& aVal) noexcept
+ : 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 = SMILNullType::Singleton();
+}
+
+SMILValue& SMILValue::operator=(SMILValue&& aVal) noexcept {
+ 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 = SMILNullType::Singleton();
+
+ return *this;
+}
+
+bool SMILValue::operator==(const SMILValue& aVal) const {
+ if (&aVal == this) return true;
+
+ return mType == aVal.mType && mType->IsEqual(*this, aVal);
+}
+
+nsresult SMILValue::Add(const SMILValue& 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 SMILValue::SandwichAdd(const SMILValue& aValueToAdd) {
+ if (aValueToAdd.mType != mType) {
+ NS_ERROR("Trying to add incompatible types");
+ return NS_ERROR_FAILURE;
+ }
+
+ return mType->SandwichAdd(*this, aValueToAdd);
+}
+
+nsresult SMILValue::ComputeDistance(const SMILValue& 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 SMILValue::Interpolate(const SMILValue& aEndVal, double aUnitDistance,
+ SMILValue& 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 SMILType::Init & ::Destroy that verify their postconditions
+void SMILValue::InitAndCheckPostcondition(const SMILType* aNewType) {
+ aNewType->Init(*this);
+ MOZ_ASSERT(mType == aNewType,
+ "Post-condition of Init failed. SMILValue is invalid");
+}
+
+void SMILValue::DestroyAndCheckPostcondition() {
+ mType->Destroy(*this);
+ MOZ_ASSERT(IsNull(),
+ "Post-condition of Destroy failed. "
+ "SMILValue not null after destroying");
+}
+
+void SMILValue::DestroyAndReinit(const SMILType* aNewType) {
+ DestroyAndCheckPostcondition();
+ InitAndCheckPostcondition(aNewType);
+}
+
+} // namespace mozilla
diff --git a/dom/smil/SMILValue.h b/dom/smil/SMILValue.h
new file mode 100644
index 0000000000..d57daa5eb3
--- /dev/null
+++ b/dom/smil/SMILValue.h
@@ -0,0 +1,75 @@
+/* -*- 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 DOM_SMIL_SMILVALUE_H_
+#define DOM_SMIL_SMILVALUE_H_
+
+#include "mozilla/SMILNullType.h"
+#include "mozilla/SMILType.h"
+
+namespace mozilla {
+
+/**
+ * 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 SMILValue 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 SMILValue objects typically live much longer
+ * than a single sample.
+ */
+class SMILValue {
+ public:
+ SMILValue() : mU(), mType(SMILNullType::Singleton()) {}
+ explicit SMILValue(const SMILType* aType);
+ SMILValue(const SMILValue& aVal);
+
+ ~SMILValue() { mType->Destroy(*this); }
+
+ const SMILValue& operator=(const SMILValue& aVal);
+
+ // Move constructor / reassignment operator:
+ SMILValue(SMILValue&& aVal) noexcept;
+ SMILValue& operator=(SMILValue&& aVal) noexcept;
+
+ // Equality operators. These are allowed to be conservative (return false
+ // more than you'd expect) - see comment above SMILType::IsEqual.
+ bool operator==(const SMILValue& aVal) const;
+ bool operator!=(const SMILValue& aVal) const { return !(*this == aVal); }
+
+ bool IsNull() const { return (mType == SMILNullType::Singleton()); }
+
+ nsresult Add(const SMILValue& aValueToAdd, uint32_t aCount = 1);
+ nsresult SandwichAdd(const SMILValue& aValueToAdd);
+ nsresult ComputeDistance(const SMILValue& aTo, double& aDistance) const;
+ nsresult Interpolate(const SMILValue& aEndVal, double aUnitDistance,
+ SMILValue& 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 SMILType* mType;
+
+ protected:
+ void InitAndCheckPostcondition(const SMILType* aNewType);
+ void DestroyAndCheckPostcondition();
+ void DestroyAndReinit(const SMILType* aNewType);
+};
+
+} // namespace mozilla
+
+#endif // DOM_SMIL_SMILVALUE_H_
diff --git a/dom/smil/TimeEvent.cpp b/dom/smil/TimeEvent.cpp
new file mode 100644
index 0000000000..104dc0d050
--- /dev/null
+++ b/dom/smil/TimeEvent.cpp
@@ -0,0 +1,60 @@
+/* -*- 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_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(TimeEvent, Event)
+
+void TimeEvent::InitTimeEvent(const nsAString& aType,
+ nsGlobalWindowInner* 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 0000000000..f0940600d8
--- /dev/null
+++ b/dom/smil/TimeEvent.h
@@ -0,0 +1,63 @@
+/* -*- 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 DOM_SMIL_TIMEEVENT_H_
+#define DOM_SMIL_TIMEEVENT_H_
+
+#include "nsDocShell.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/TimeEventBinding.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/WindowProxyHolder.h"
+
+class nsGlobalWindowInner;
+
+namespace mozilla {
+namespace dom {
+
+class TimeEvent final : public Event {
+ public:
+ TimeEvent(EventTarget* aOwner, nsPresContext* aPresContext,
+ InternalSMILTimeEvent* aEvent);
+
+ // nsISupports interface:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TimeEvent, Event)
+
+ virtual JSObject* WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override {
+ return TimeEvent_Binding::Wrap(aCx, this, aGivenProto);
+ }
+
+ void InitTimeEvent(const nsAString& aType, nsGlobalWindowInner* aView,
+ int32_t aDetail);
+
+ int32_t Detail() const { return mDetail; }
+
+ Nullable<WindowProxyHolder> GetView() const {
+ if (!mView) {
+ return nullptr;
+ }
+ return WindowProxyHolder(mView->GetBrowsingContext());
+ }
+
+ TimeEvent* AsTimeEvent() final { return this; }
+
+ private:
+ ~TimeEvent() = default;
+
+ 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 // DOM_SMIL_TIMEEVENT_H_
diff --git a/dom/smil/crashtests/1010681-1.svg b/dom/smil/crashtests/1010681-1.svg
new file mode 100644
index 0000000000..882bcb53fe
--- /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.requiredExtensions.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/1322770-1.svg b/dom/smil/crashtests/1322770-1.svg
new file mode 100644
index 0000000000..405435184e
--- /dev/null
+++ b/dom/smil/crashtests/1322770-1.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg">
+ <animateMotion keyTimes='.1;.6;.6' path='m0,1l7,3' keyPoints='.6;1;.7'/>
+</svg>
diff --git a/dom/smil/crashtests/1322849-1.svg b/dom/smil/crashtests/1322849-1.svg
new file mode 100644
index 0000000000..ea3d629813
--- /dev/null
+++ b/dom/smil/crashtests/1322849-1.svg
@@ -0,0 +1,2 @@
+<svg>
+<set fill='freeze' dur='8' repeatCount='1844674737095516'>
diff --git a/dom/smil/crashtests/1343357-1.html b/dom/smil/crashtests/1343357-1.html
new file mode 100644
index 0000000000..8219c31221
--- /dev/null
+++ b/dom/smil/crashtests/1343357-1.html
@@ -0,0 +1,12 @@
+<svg>
+ <animateMotion to="500,500"></animateMotion>
+ <animateMotion to="10,40"></animateMotion>
+</svg>
+<svg width="100" height="100">
+ <rect width="100%" height="100%" />
+ <circle r="2" fill="red">
+ <animateMotion dur="1s" from="50,50" to="80,70" additive="sum"></animateMotion>
+ <animateMotion dur="1s" from="50,50" to="80,70" additive="sum"></animateMotion>
+ <animateMotion dur="3s" to="0,80"></animateMotion>
+ </circle>
+</svg>
diff --git a/dom/smil/crashtests/1375596-1.svg b/dom/smil/crashtests/1375596-1.svg
new file mode 100644
index 0000000000..69c1673d11
--- /dev/null
+++ b/dom/smil/crashtests/1375596-1.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600">
+<animate by="2" min="5:45" calcMode="discrete" attributeName="height" />
+</svg>
diff --git a/dom/smil/crashtests/1402547-1.html b/dom/smil/crashtests/1402547-1.html
new file mode 100644
index 0000000000..28fa7ce185
--- /dev/null
+++ b/dom/smil/crashtests/1402547-1.html
@@ -0,0 +1,3 @@
+<svg>
+<animate id='a' calcMode='discrete' attributeName='height' by='-159'/>
+<animate id='a' calcMode='discrete' attributeName='height' by='-159'/>
diff --git a/dom/smil/crashtests/1411963-1.html b/dom/smil/crashtests/1411963-1.html
new file mode 100644
index 0000000000..cc61b73e2a
--- /dev/null
+++ b/dom/smil/crashtests/1411963-1.html
@@ -0,0 +1,10 @@
+<html>
+ <head>
+ <script>
+ const o1 = document.createElement('div');
+ document.querySelector('script').appendChild(o1);
+ document.writeln("<svg><animate to attributeName='width'>");
+ o1.innerHTML = "<meta http-equiv='Content-Security-Policy' content=default-src>";
+ </script>
+ </head>
+</html>
diff --git a/dom/smil/crashtests/1413319-1.html b/dom/smil/crashtests/1413319-1.html
new file mode 100644
index 0000000000..9bfeef3bdd
--- /dev/null
+++ b/dom/smil/crashtests/1413319-1.html
@@ -0,0 +1,2 @@
+<svg width=''>
+<animate dur='2ms' repeatCount='4611686018427387903' fill='freeze'/>
diff --git a/dom/smil/crashtests/1535388-1.html b/dom/smil/crashtests/1535388-1.html
new file mode 100644
index 0000000000..cdfaba4d90
--- /dev/null
+++ b/dom/smil/crashtests/1535388-1.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+<script>
+function start () {
+ document.location.assign('abc')
+ SpecialPowers.forceGC()
+ SpecialPowers.forceCC()
+ const XHR = new XMLHttpRequest()
+ XHR.open('GET', 'data:text/html,1', false)
+ XHR.send()
+}
+window.addEventListener('load', start)
+</script>
+</head>
+<body>
+<svg></svg>
+</body>
+</html>
diff --git a/dom/smil/crashtests/483584-1.svg b/dom/smil/crashtests/483584-1.svg
new file mode 100644
index 0000000000..b9ded113ef
--- /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 0000000000..f5cbd7d466
--- /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 0000000000..c03cea4923
--- /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 0000000000..8eed11489a
--- /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 0000000000..4fcf35d081
--- /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 0000000000..281454bf61
--- /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 0000000000..73c229da5f
--- /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 0000000000..7885ab71fd
--- /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 0000000000..de3dbec34c
--- /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 0000000000..306f41702d
--- /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/541297-1.svg b/dom/smil/crashtests/541297-1.svg
new file mode 100644
index 0000000000..4268232ba1
--- /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 0000000000..bac629b493
--- /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 0000000000..c12ed27454
--- /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 0000000000..2ea83e9c29
--- /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 0000000000..61ce419f53
--- /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-2.svg b/dom/smil/crashtests/554202-2.svg
new file mode 100644
index 0000000000..a3bbb3195c
--- /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 0000000000..76b4cf0756
--- /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 0000000000..92712deaa9
--- /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 0000000000..d759944c7d
--- /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 0000000000..8b9cf7b70e
--- /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 0000000000..642ad32fba
--- /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 0000000000..549d43dd62
--- /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 0000000000..cc35cf6b46
--- /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 0000000000..70d8e76391
--- /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 0000000000..906d348db2
--- /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.remove();
+ frameSVG.appendChild(animate);
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+<body onload="boom()">
+
+<iframe id="frame" srcdoc="<body><svg id=s>"></iframe>
+
+</body>
+</html>
diff --git a/dom/smil/crashtests/592477-1.xhtml b/dom/smil/crashtests/592477-1.xhtml
new file mode 100644
index 0000000000..c44a470a39
--- /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 0000000000..76352ce30b
--- /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 0000000000..52a66fd582
--- /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 0000000000..94887cf713
--- /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 0000000000..988c86fa33
--- /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 0000000000..354e6f9099
--- /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 0000000000..dd441e0135
--- /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 0000000000..ea60f4ce1c
--- /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 0000000000..eb9a293199
--- /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 0000000000..e0cdf21546
--- /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 0000000000..25c941dedb
--- /dev/null
+++ b/dom/smil/crashtests/641388-1.html
@@ -0,0 +1,97 @@
+<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{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 0000000000..f2ddead7e1
--- /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(); });
+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 0000000000..95be31c16a
--- /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 0000000000..94916d1e0e
--- /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 0000000000..9660105631
--- /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 0000000000..00d52c1f4c
--- /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 0000000000..97e12f35ac
--- /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 0000000000..a5e81ee10f
--- /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 0000000000..1fa2718cbb
--- /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 0000000000..f3f8308fa5
--- /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 0000000000..252fd2c264
--- /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 0000000000..c341faa6b2
--- /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 0000000000..f4408ae5ee
--- /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 0000000000..c2e1b89fdb
--- /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 0000000000..7496c6ae21
--- /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 0000000000..631911970c
--- /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 0000000000..a51a3bf0fc
--- /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/849593-1.xhtml b/dom/smil/crashtests/849593-1.xhtml
new file mode 100644
index 0000000000..95b9b2feb8
--- /dev/null
+++ b/dom/smil/crashtests/849593-1.xhtml
@@ -0,0 +1,34 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head class="reftest-wait">
+<meta charset="utf-8"/>
+<script>
+<![CDATA[
+
+function boom()
+{
+ var a = document.getElementById('a');
+ var b = document.getElementById('b');
+ var c = document.getElementById('c');
+ var d = document.getElementById('d');
+
+ b.setCurrentTime(1);
+
+ a.appendChild(c);
+ document.body.removeChild(a);
+ b.appendChild(c);
+ document.documentElement.offsetHeight;
+ d.appendChild(a);
+
+ document.documentElement.removeAttribute('class');
+}
+
+]]>
+</script>
+</head>
+
+<body onload="setTimeout(boom, 0);">
+<svg xmlns="http://www.w3.org/2000/svg" id="a"/>
+<svg xmlns="http://www.w3.org/2000/svg" id="b"/>
+<set xmlns="http://www.w3.org/2000/svg" begin="1s" id="c"><div xmlns="http://www.w3.org/1999/xhtml" id="d"></div></set>
+</body>
+</html>
diff --git a/dom/smil/crashtests/crashtests.list b/dom/smil/crashtests/crashtests.list
new file mode 100644
index 0000000000..844cfad672
--- /dev/null
+++ b/dom/smil/crashtests/crashtests.list
@@ -0,0 +1,61 @@
+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 541297-1.svg
+load 547333-1.svg
+load 548899-1.svg
+load 551620-1.svg
+load 554141-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 849593-1.xhtml
+load 1010681-1.svg
+load 1322770-1.svg
+load 1322849-1.svg
+load 1343357-1.html
+load 1375596-1.svg
+load 1402547-1.html
+load 1411963-1.html
+load 1413319-1.html
+load 1535388-1.html
diff --git a/dom/smil/moz.build b/dom/smil/moz.build
new file mode 100644
index 0000000000..c6c3bd7351
--- /dev/null
+++ b/dom/smil/moz.build
@@ -0,0 +1,74 @@
+# -*- 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "SVG")
+
+MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+
+EXPORTS.mozilla += [
+ "SMILAnimationController.h",
+ "SMILAnimationFunction.h",
+ "SMILAttr.h",
+ "SMILCompositorTable.h",
+ "SMILCSSValueType.h",
+ "SMILInstanceTime.h",
+ "SMILInterval.h",
+ "SMILKeySpline.h",
+ "SMILMilestone.h",
+ "SMILNullType.h",
+ "SMILParserUtils.h",
+ "SMILRepeatCount.h",
+ "SMILSetAnimationFunction.h",
+ "SMILTargetIdentifier.h",
+ "SMILTimeContainer.h",
+ "SMILTimedElement.h",
+ "SMILTimeValue.h",
+ "SMILTimeValueSpec.h",
+ "SMILTimeValueSpecParams.h",
+ "SMILType.h",
+ "SMILTypes.h",
+ "SMILValue.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "TimeEvent.h",
+]
+
+UNIFIED_SOURCES += [
+ "SMILAnimationController.cpp",
+ "SMILAnimationFunction.cpp",
+ "SMILBoolType.cpp",
+ "SMILCompositor.cpp",
+ "SMILCSSProperty.cpp",
+ "SMILCSSValueType.cpp",
+ "SMILEnumType.cpp",
+ "SMILFloatType.cpp",
+ "SMILInstanceTime.cpp",
+ "SMILIntegerType.cpp",
+ "SMILInterval.cpp",
+ "SMILKeySpline.cpp",
+ "SMILNullType.cpp",
+ "SMILParserUtils.cpp",
+ "SMILRepeatCount.cpp",
+ "SMILSetAnimationFunction.cpp",
+ "SMILStringType.cpp",
+ "SMILTimeContainer.cpp",
+ "SMILTimedElement.cpp",
+ "SMILTimeValue.cpp",
+ "SMILTimeValueSpec.cpp",
+ "SMILValue.cpp",
+ "TimeEvent.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/dom/svg",
+ "/layout/base",
+ "/layout/style",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/smil/test/.eslintrc.js b/dom/smil/test/.eslintrc.js
new file mode 100644
index 0000000000..845ed3f013
--- /dev/null
+++ b/dom/smil/test/.eslintrc.js
@@ -0,0 +1,5 @@
+"use strict";
+
+module.exports = {
+ extends: ["plugin:mozilla/mochitest-test"],
+};
diff --git a/dom/smil/test/db_smilAnimateMotion.js b/dom/smil/test/db_smilAnimateMotion.js
new file mode 100644
index 0000000000..31c338586f
--- /dev/null
+++ b/dom/smil/test/db_smilAnimateMotion.js
@@ -0,0 +1,309 @@
+/* -*- 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 0000000000..737037271d
--- /dev/null
+++ b/dom/smil/test/db_smilCSSFromBy.js
@@ -0,0 +1,207 @@
+/* -*- 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)",
+ }
+ ),
+
+ // The "from" and "by" values in the test case below 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.
+ //
+ // That gives us:
+ //
+ // to-value = (rgb(100, 100, 100) * 0.6 + rgb(240, 240, 240) * 1.0)) * 1
+ // = rgb(300, 300, 300)
+ // midComp = (rgb(100, 100, 100) * 0.6 * 0.5 + rgb(300, 300, 300) * 1.0 * 0.5) * (1 / 0.8)
+ // = rgb(225, 225, 225)
+ //
+ //
+ new AnimTestcaseFromBy(
+ "rgba(100, 100, 100, 0.6)",
+ "rgba(240, 240, 240, 1)",
+ { midComp: "rgba(225, 225, 225, 0.8)", 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",
+ }),
+ ],
+ lengthPx: [
+ new AnimTestcaseFromBy("0px", "8px", {
+ fromComp: "0px",
+ midComp: "4px",
+ toComp: "8px",
+ }),
+ new AnimTestcaseFromBy("1px", "10px", {
+ fromComp: "1px",
+ midComp: "6px",
+ toComp: "11px",
+ }),
+ ],
+ 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"),
+ ]),
+ // Bug 1457353: Change from nsColor to StyleComplexColor causes addition
+ // with currentcolor to break. Bug 1465307 for work to re-enable.
+ 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.lengthNoUnits, _fromByTestLists.lengthPx)
+ ),
+];
diff --git a/dom/smil/test/db_smilCSSFromTo.js b/dom/smil/test/db_smilCSSFromTo.js
new file mode 100644
index 0000000000..daa75eccd1
--- /dev/null
+++ b/dom/smil/test/db_smilCSSFromTo.js
@@ -0,0 +1,625 @@
+/* -*- 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",
+ }),
+ ],
+ lengthPx: [
+ new AnimTestcaseFromTo("0px", "12px", {
+ fromComp: "0px",
+ midComp: "6px",
+ toComp: "12px",
+ }),
+ new AnimTestcaseFromTo("16px", "0px", {
+ fromComp: "16px",
+ midComp: "8px",
+ toComp: "0px",
+ }),
+ new AnimTestcaseFromTo("10px", "20px", {
+ fromComp: "10px",
+ midComp: "15px",
+ toComp: "20px",
+ }),
+ new AnimTestcaseFromTo("41px", "1px", {
+ fromComp: "41px",
+ midComp: "21px",
+ toComp: "1px",
+ }),
+ ],
+ 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: "10px",
+ midComp: "15px",
+ toComp: "20px",
+ }),
+ new AnimTestcaseFromTo("10px", "20", {
+ fromComp: "10px",
+ midComp: "15px",
+ toComp: "20px",
+ }),
+ ],
+ 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",
+ }),
+ ],
+};
+
+function _tweakForLetterSpacing(testcases) {
+ return testcases.map(function(t) {
+ let valMap = Object.assign({}, t.computedValMap);
+ for (let prop of Object.keys(valMap)) {
+ if (valMap[prop] == "0px") {
+ valMap[prop] = "normal";
+ }
+ }
+ return new AnimTestcaseFromTo(t.from, t.to, valMap);
+ });
+}
+
+// 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("alphabetic", "hanging"),
+ new AnimTestcaseFromTo("mathematical", "central"),
+ new AnimTestcaseFromTo("middle", "text-after-edge"),
+ new AnimTestcaseFromTo("text-before-edge", "auto"),
+ new AnimTestcaseFromTo("alphabetic", "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", {
+ fromComp: "50%",
+ midComp: "62.5%",
+ toComp: "75%",
+ }),
+ new AnimTestcaseFromTo("semi-condensed", "semi-expanded", {
+ fromComp: "87.5%",
+ midComp: "100%",
+ toComp: "112.5%",
+ }),
+ new AnimTestcaseFromTo("expanded", "ultra-expanded", {
+ fromComp: "125%",
+ midComp: "162.5%",
+ toComp: "200%",
+ }),
+ new AnimTestcaseFromTo("ultra-expanded", "inherit", {
+ fromComp: "200%",
+ midComp: "150%",
+ toComp: "100%",
+ }),
+ ]),
+ 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: "550",
+ 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,
+ _tweakForLetterSpacing(
+ [].concat(_fromToTestLists.lengthNoUnits, _fromToTestLists.lengthPx)
+ )
+ ),
+ 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.lengthNoUnits,
+ _fromToTestLists.lengthPx,
+ _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.lengthNoUnits,
+ _fromToTestLists.lengthPx,
+ _fromToTestLists.lengthPxPctSVG,
+ _fromToTestLists.lengthPctSVG,
+ _fromToTestLists.lengthPxNoUnitsSVG,
+ [
+ new AnimTestcaseFromTo("inherit", "7px", {
+ fromComp: "1px",
+ midComp: "4px",
+ toComp: "7px",
+ }),
+ ]
+ )
+ ),
+ new TestcaseBundle(gPropList.text_anchor, [
+ new AnimTestcaseFromTo("start", "middle"),
+ new AnimTestcaseFromTo("middle", "end"),
+ ]),
+ new TestcaseBundle(gPropList.text_decoration_line, [
+ 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 0000000000..e5212dd80e
--- /dev/null
+++ b/dom/smil/test/db_smilCSSPaced.js
@@ -0,0 +1,356 @@
+/* -*- 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)",
+ }
+ ),
+ // 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)",
+ }
+ ),
+ ],
+ currentColor_color: [
+ 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)",
+ }
+ ),
+ ],
+ currentColor_fill: [
+ // Bug 1467622 changed the distance calculation
+ // involving currentColor, comp values below
+ // are no longer evenly spaced.
+ new AnimTestcasePaced(
+ "olive; " + // rgb(128, 128, 0)
+ "currentColor; " + // rgb(50, 50, 50)
+ "rgb(206, 150, 206)",
+ {
+ comp0: "rgb(128, 128, 0)",
+ comp1_6: "rgb(99, 99, 18)",
+ comp1_3: "rgb(71, 71, 37)",
+ comp2_3: "rgb(111, 89, 111)",
+ comp1: "rgb(206, 150, 206)",
+ }
+ ),
+ ],
+ 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",
+ }),
+ ],
+ 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",
+ }),
+ ],
+ 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,
+ [].concat(_pacedTestLists.color, _pacedTestLists.currentColor_color)
+ ),
+ new TestcaseBundle(gPropList.direction, [
+ new AnimTestcasePaced("rtl; ltr; rtl"),
+ ]),
+ new TestcaseBundle(
+ gPropList.fill,
+ [].concat(
+ _pacedTestLists.color,
+ _pacedTestLists.currentColor_fill,
+ _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: "7px, 7px, 7px",
+ comp1_6: "7px, 8.5px, 5px",
+ comp1_3: "7px, 10px, 3px",
+ comp2_3: "4px, 6px, 3px",
+ comp1: "1px, 2px, 3px",
+ }),
+ ])
+ ),
+ new TestcaseBundle(
+ gPropList.stroke_dashoffset,
+ [].concat(
+ _pacedTestLists.lengthNoUnits,
+ _pacedTestLists.lengthPx,
+ _pacedTestLists.lengthPctSVG,
+ _pacedTestLists.lengthPxPctSVG
+ )
+ ),
+ new TestcaseBundle(
+ gPropList.stroke_width,
+ [].concat(
+ _pacedTestLists.lengthNoUnits,
+ _pacedTestLists.lengthPx,
+ _pacedTestLists.lengthPctSVG,
+ _pacedTestLists.lengthPxPctSVG
+ )
+ ),
+];
diff --git a/dom/smil/test/db_smilCSSPropertyList.js b/dom/smil/test/db_smilCSSPropertyList.js
new file mode 100644
index 0000000000..237c9db585
--- /dev/null
+++ b/dom/smil/test/db_smilCSSPropertyList.js
@@ -0,0 +1,104 @@
+/* -*- 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
+ 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_line: new NonAdditiveAttribute(
+ "text-decoration-line",
+ "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 0000000000..81f71ef32b
--- /dev/null
+++ b/dom/smil/test/db_smilMappedAttrList.js
@@ -0,0 +1,148 @@
+/* -*- 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
+ // SVGElement.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_line: new NonAdditiveAttribute(
+ "text-decoration-line",
+ "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/file_smilWithTransition.html b/dom/smil/test/file_smilWithTransition.html
new file mode 100644
index 0000000000..b91398436b
--- /dev/null
+++ b/dom/smil/test/file_smilWithTransition.html
@@ -0,0 +1,79 @@
+<!doctype html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1315874
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test SMIL does not trigger CSS Transitions (bug 1315874)</title>
+</head>
+<body>
+<a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315874">Mozilla Bug
+ 1315874</a>
+<svg>
+ <rect width="100%" height="100%"
+ style="fill: red; transition: fill 10s" id="rect">
+ <animate attributeName="fill" to="lime" dur="1s" fill="freeze">
+ </rect>
+</svg>
+<script type="text/javascript">
+ // Bring SimpleTest's function from opener.
+ if (opener) {
+ var is = opener.is.bind(opener);
+ var ok = opener.ok.bind(opener);
+ function finish() {
+ var o = opener;
+ self.close();
+ o.SimpleTest.finish();
+ }
+ }
+
+ window.addEventListener('load', runTests);
+
+ var rect = document.getElementById('rect');
+ var svg = document.getElementsByTagName('svg')[0];
+ is(getComputedStyle(rect).fill, 'rgb(255, 0, 0)',
+ 'The initial color should be red.');
+
+ function runTests() {
+ waitForFrame().then(function() {
+ svg.setCurrentTime(1);
+ is(getComputedStyle(rect).fill, 'rgb(0, 255, 0)',
+ 'The end color should be lime.');
+
+ return waitForAnimationFrames(2);
+ }).then(function() {
+ var anim = document.getAnimations()[0];
+ ok(!anim, 'Transition should not be created by restyling for SMIL');
+ finish();
+ });
+ }
+
+ // Utility methods from testcommon.js
+ // For detail, see dom/animation/test/testcommon.js.
+
+ function waitForFrame() {
+ return new Promise(function(resolve, reject) {
+ requestAnimationFrame(function(time) {
+ resolve();
+ });
+ });
+ }
+
+ function waitForAnimationFrames(frameCount) {
+ return new Promise(function(resolve, reject) {
+ function handleFrame() {
+ if (--frameCount <= 0) {
+ resolve();
+ } else {
+ window.requestAnimationFrame(handleFrame);
+ }
+ }
+ window.requestAnimationFrame(handleFrame);
+ });
+ }
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/smil/test/mochitest.ini b/dom/smil/test/mochitest.ini
new file mode 100644
index 0000000000..1b81646dbc
--- /dev/null
+++ b/dom/smil/test/mochitest.ini
@@ -0,0 +1,63 @@
+[DEFAULT]
+support-files =
+ db_smilAnimateMotion.js
+ db_smilCSSFromBy.js
+ db_smilCSSFromTo.js
+ db_smilCSSPaced.js
+ db_smilCSSPropertyList.js
+ db_smilMappedAttrList.js
+ file_smilWithTransition.html
+ smilAnimateMotionValueLists.js
+ smilExtDoc_helper.svg
+ smilTestUtils.js
+ smilXHR_helper.svg
+
+[test_smilAccessKey.xhtml]
+[test_smilAdditionFallback.html]
+[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]
+skip-if = true # bug 1358955.
+[test_smilConditionalProcessing.html]
+[test_smilContainerBinding.xhtml]
+[test_smilCrossContainer.xhtml]
+[test_smilDynamicDelayedBeginElement.xhtml]
+[test_smilExtDoc.xhtml]
+[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]
+[test_smilReset.xhtml]
+[test_smilRestart.xhtml]
+[test_smilSetCurrentTime.xhtml]
+[test_smilSync.xhtml]
+[test_smilSyncTransform.xhtml]
+[test_smilSyncbaseTarget.xhtml]
+[test_smilTextZoom.xhtml]
+[test_smilTiming.xhtml]
+[test_smilTimingZeroIntervals.xhtml]
+[test_smilUpdatedInterval.xhtml]
+[test_smilValues.xhtml]
+[test_smilWithTransition.html]
+skip-if = toolkit == 'android' && !is_fennec
+[test_smilWithXlink.xhtml]
+[test_smilXHR.xhtml]
diff --git a/dom/smil/test/smilAnimateMotionValueLists.js b/dom/smil/test/smilAnimateMotionValueLists.js
new file mode 100644
index 0000000000..6e05ebd7e1
--- /dev/null
+++ b/dom/smil/test/smilAnimateMotionValueLists.js
@@ -0,0 +1,116 @@
+/* -*- 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",
+ " 10 ",
+ " 10deg",
+ "10deg ",
+ " 10.1 ",
+];
+
+const gInvalidRotate = ["10 deg", "10 rad ", "aaa"];
+
+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 0000000000..fbd9d091a4
--- /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 0000000000..98aa3750bd
--- /dev/null
+++ b/dom/smil/test/smilTestUtils.js
@@ -0,0 +1,1014 @@
+/* -*- 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() {
+ return SMILUtil.getFirstElemWithTag("svg");
+ },
+
+ // Returns the first element in the document with the matching tag
+ getFirstElemWithTag(aTargetTag) {
+ var elemList = document.getElementsByTagName(aTargetTag);
+ return elemList.length == 0 ? null : elemList[0];
+ },
+
+ // Simple wrapper for getComputedStyle
+ getComputedStyleSimple(elem, prop) {
+ return window.getComputedStyle(elem).getPropertyValue(prop);
+ },
+
+ getAttributeValue(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(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() {
+ return "_motion";
+ },
+
+ // Return stripped px value from specified value.
+ stripPx: str => str.replace(/px\s*$/, ""),
+};
+
+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(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(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(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(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() {
+ return this._begin;
+ },
+ getDur() {
+ return this._dur;
+ },
+ getEndTime() {
+ return this._begin + this._dur;
+ },
+ getFractionalTime(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(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(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(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(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(aSeekList, aTargetElem, aTargetAttr) {
+ var svg = document.getElementById("svg");
+ for (var i in aSeekList) {
+ var entry = aSeekList[i];
+ SMILUtil.getSVGRoot().setCurrentTime(entry[0]);
+
+ // Bug 1379908: The computed value of stroke-* properties should be
+ // serialized with px units, but currently Gecko and Servo don't do that
+ // when animating these values.
+ if (
+ ["stroke-width", "stroke-dasharray", "stroke-dashoffset"].includes(
+ aTargetAttr.attrName
+ )
+ ) {
+ var attr = SMILUtil.stripPx(
+ SMILUtil.getAttributeValue(aTargetElem, aTargetAttr)
+ );
+ var expectedVal = SMILUtil.stripPx(entry[1]);
+ is(attr, expectedVal, entry[2]);
+ return;
+ }
+ is(
+ SMILUtil.getAttributeValue(aTargetElem, aTargetAttr),
+ entry[1],
+ entry[2]
+ );
+ }
+ },
+
+ // methods that expect to be overridden in subclasses
+ buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) {},
+ buildSeekListAnimated(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(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(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(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(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(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(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(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(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(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(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(aPathDescription) {
+ var path = document.createElementNS(SVG_NS, "path");
+ path.setAttribute("d", aPathDescription);
+ path.setAttribute("id", MPATH_TARGET_ID);
+ return SMILUtil.getSVGRoot().appendChild(path);
+ },
+
+ createMpath(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(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(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(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 0000000000..cb0b51c360
--- /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 0000000000..5910dc1c27
--- /dev/null
+++ b/dom/smil/test/test_smilAccessKey.xhtml
@@ -0,0 +1,79 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL accessKey support</title>
+ <script 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 lack of SMIL accessKey support **/
+
+const gSvgns = 'http://www.w3.org/2000/svg';
+SimpleTest.waitForExplicitFinish();
+
+function main()
+{
+ testBeginValueIsNotSupported('accessKey(a)');
+ testBeginValueIsNotSupported('accesskey(a)');
+
+ is(getStartTime('accesskey(a); 1s'), 1,
+ 'Start time for accesskey(a) followed by a literal time');
+ is(getStartTime('3s; accessKey(a)'), 3,
+ 'Start time for accesskey(a) preceded by a literal time');
+
+ SimpleTest.finish();
+}
+
+function createAnim(beginSpec) {
+ const 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 testBeginValueIsNotSupported(beginSpec) {
+ const anim = createAnim(beginSpec);
+
+ try {
+ anim.getStartTime();
+ ok(false,
+ `Should have failed to get start time for begin value: ${beginSpec}`);
+ } catch(e) {
+ is(e.name, 'InvalidStateError', `Unexpected exception: ${e.name}`);
+ is(e.code, DOMException.INVALID_STATE_ERR,
+ `Unexpected exception code: ${e.code}`);
+ }
+
+ anim.remove();
+}
+
+function getStartTime(beginSpec) {
+ const anim = createAnim(beginSpec);
+ let startTime;
+ try {
+ startTime = anim.getStartTime();
+ } catch (e) { }
+ anim.remove();
+
+ return startTime;
+}
+
+window.addEventListener('load', main);
+]]>
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/smil/test/test_smilAdditionFallback.html b/dom/smil/test/test_smilAdditionFallback.html
new file mode 100644
index 0000000000..a73457728c
--- /dev/null
+++ b/dom/smil/test/test_smilAdditionFallback.html
@@ -0,0 +1,30 @@
+<!doctype html>
+<meta charset=utf-8>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<p id=display></p>
+<div id=content>
+<svg id=svg>
+<!-- These two animations will have a default duration of indefinite which means
+ they will keep producing their first value forever -->
+<animate calcMode="discrete" attributeName="height" by="10" dur="1s"
+ fill="freeze"/>
+<animate calcMode="discrete" attributeName="height" by="10" dur="1s"
+ fill="freeze"/>
+</svg>
+</div>
+<pre id="test">
+<script>
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+
+window.addEventListener('load', () => {
+ const svg = document.getElementById('svg');
+ is(getComputedStyle(svg).height, '0px', 'Computed height should be zero');
+ SimpleTest.finish();
+});
+</script>
+</pre>
diff --git a/dom/smil/test/test_smilAnimateMotion.xhtml b/dom/smil/test/test_smilAnimateMotion.xhtml
new file mode 100644
index 0000000000..68ac7fb96a
--- /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 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);
+]]>
+</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 0000000000..c2d72e5435
--- /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 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.remove();
+}
+
+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);
+]]>
+</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 0000000000..3a147cd094
--- /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 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.remove();
+}
+
+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);
+]]>
+</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 0000000000..20974e6de3
--- /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 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.remove();
+ animB.remove();
+ }
+
+ 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);
+]]>
+</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 0000000000..01988a881b
--- /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 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", "50%"],
+ ["extra-condensed", "62.5%"],
+ ["condensed", "75%"],
+ ["semi-condensed", "87.5%"],
+ ["normal", "100%"],
+ ["semi-expanded", "112.5%"],
+ ["expanded", "125%"],
+ ["extra-expanded", "150%"],
+ ["ultra-expanded", "200%"],
+];
+
+function testFontStretchValue([baseValue, computedValue], [narrowerStep, computedNarrowerStep], [widerStep, computedWiderStep])
+{
+ 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), computedNarrowerStep,
+ "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), computedWiderStep,
+ "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);
+]]>
+</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 0000000000..586305fb8a
--- /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 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);
+]]>
+</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 0000000000..501adbc4a8
--- /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 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);
+]]>
+</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 0000000000..4e262d3aa9
--- /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 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);
+]]>
+</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 0000000000..5e8e682af9
--- /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 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);
+]]>
+</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 0000000000..1f6b3509ae
--- /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 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);
+]]>
+</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 0000000000..97fe131678
--- /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 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.remove(); // 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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+function testCalcFontSize()
+{
+ setupTest();
+ gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px
+ var anim = createAnimSetTo("font-size", "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.firstChild.remove();
+}
+
+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.firstChild.remove();
+}
+
+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);
+]]>
+</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 0000000000..302c445b6e
--- /dev/null
+++ b/dom/smil/test/test_smilConditionalProcessing.html
@@ -0,0 +1,93 @@
+<!doctype html>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test conditional processing tests applied to animations</title>
+ <script 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">
+
+SimpleTest.waitForExplicitFinish();
+
+function run() {
+ SpecialPowers.pushPrefEnv({"set": [["intl.accept_languages", "en"]]}, runInternal);
+}
+
+function runInternal() {
+ 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");
+
+ SimpleTest.finish();
+}
+
+function hasUnresolvedStartTime(anim) {
+ // getStartTime throws INVALID_STATE_ERR when there is no current interval
+ try {
+ anim.getStartTime();
+ return false;
+ } catch (e) {
+ return true;
+ }
+}
+
+window.addEventListener("load", run);
+
+</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 0000000000..20e1013d4b
--- /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 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.remove();
+ 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.remove();
+ 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);
+]]>
+</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 0000000000..e4d30ef26d
--- /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 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.remove();
+ 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);
+]]>
+</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 0000000000..10da496454
--- /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 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);
+
+]]>
+</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 0000000000..0323cbfb77
--- /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 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);
+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 0000000000..ed270863bd
--- /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 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.remove();
+ }
+ 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);
+]]>
+</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 0000000000..a54139a898
--- /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 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);
+]]>
+</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 0000000000..f854dd7cd9
--- /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 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);
+]]>
+</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 0000000000..38e4bd3b9b
--- /dev/null
+++ b/dom/smil/test/test_smilHyperlinking.xhtml
@@ -0,0 +1,229 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for hyperlinking</title>
+ <script 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.remove();
+ }
+ 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() {
+ window.setTimeout(fireLinkPart2, 0, callback);
+ }, {once: true});
+ }
+}
+
+function fireLinkPart2(callback) {
+ window.addEventListener("hashchange",
+ function() {
+ window.setTimeout(callback, 0);
+ }, {once: true});
+ window.location.hash = '#anim';
+}
+
+window.addEventListener("load", continueTest);
+]]>
+</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 0000000000..d6a262fc8d
--- /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 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);
+
+ // 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 0000000000..c12f6f1e09
--- /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 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.remove();
+ }
+ 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 -- SMILKeySpline(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.remove();
+ }
+}
+
+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.remove();
+ }
+}
+
+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);
+]]>
+</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 0000000000..43d3c91895
--- /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 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.remove();
+ }
+
+ // 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.remove();
+
+ 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).
+ getPropertyValue('stroke-linecap');
+ is(actualValue, expectedValue, msg);
+}
+
+window.addEventListener("load", main);
+]]>
+</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 0000000000..f2d6571fa8
--- /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 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.remove(); // 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.remove(); // 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.remove(); // 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);
+]]>
+</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 0000000000..5b70b5b83f
--- /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 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);
+]]>
+</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 0000000000..57ef83800e
--- /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 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);
+]]>
+</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 0000000000..81adea6390
--- /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 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);
+]]>
+</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 0000000000..f0bdd42502
--- /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 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);
+
+ 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 0000000000..b746bb45d4
--- /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 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);
+
+ // 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 0000000000..ace63d37b0
--- /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 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);
+]]>
+</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 0000000000..a53f73c112
--- /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 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);
+]]>
+</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 0000000000..3e03dfdbcc
--- /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 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);
+]]>
+</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 0000000000..91ded84c8c
--- /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 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);
+]]>
+</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 0000000000..36b2a91198
--- /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 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);
+]]>
+</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 0000000000..79c5cbf0b9
--- /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 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);
+]]>
+</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 0000000000..496cb6751e
--- /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 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.remove();
+}
+
+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.remove();
+}
+
+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.remove();
+ 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.remove();
+ 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);
+]]>
+</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 0000000000..5f65bd778e
--- /dev/null
+++ b/dom/smil/test/test_smilTextZoom.xhtml
@@ -0,0 +1,99 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for SMIL Animation Behavior with textZoom</title>
+ <script 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);
+
+ // Bug 1379908: The computed value of stroke-* properties should be
+ // serialized with px units, but currently Gecko and Servo don't do that
+ // when animating these values.
+ if ('stroke-width' == aPropertyName) {
+ var expectedVal = SMILUtil.stripPx(aExpectedVal);
+ var unitlessComputedVal = SMILUtil.stripPx(computedVal);
+ is(unitlessComputedVal, expectedVal, "computed value of " + aPropertyName);
+ return;
+ }
+ 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", "20px");
+ svg.setCurrentTime(1.5);
+ verifyStyle(text, "font-size", "30px");
+ verifyStyle(rect, "stroke-width", "30px");
+ svg.setCurrentTime(2);
+ verifyStyle(text, "font-size", "40px");
+ verifyStyle(rect, "stroke-width", "40px");
+ svg.setCurrentTime(3);
+ verifyStyle(text, "font-size", "40px");
+ verifyStyle(rect, "stroke-width", "40px");
+ } 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);
+]]>
+</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 0000000000..0dc8525382
--- /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 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;;', 3));
+ testCases.push(StartTimeTest('3;; ', 3));
+ testCases.push(StartTimeTest(';3', 3));
+ testCases.push(StartTimeTest(' ;3', 3));
+ 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.remove();
+ }
+
+ 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);
+]]>
+</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 0000000000..d54b74600b
--- /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 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.remove();
+}
+
+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);
+]]>
+</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 0000000000..3045a815de
--- /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 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);
+]]>
+</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 0000000000..b25a153472
--- /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 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.remove();
+ }
+
+ 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);
+]]>
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/smil/test/test_smilWithTransition.html b/dom/smil/test/test_smilWithTransition.html
new file mode 100644
index 0000000000..f464f9a39d
--- /dev/null
+++ b/dom/smil/test/test_smilWithTransition.html
@@ -0,0 +1,18 @@
+<!doctype html>
+<meta charset=utf-8>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<pre id="test">
+<script>
+'use strict';
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ { "set": [["dom.animations-api.getAnimations.enabled", true]]},
+ function() {
+ window.open("file_smilWithTransition.html");
+ });
+</script>
+</html>
diff --git a/dom/smil/test/test_smilWithXlink.xhtml b/dom/smil/test/test_smilWithXlink.xhtml
new file mode 100644
index 0000000000..16c6016deb
--- /dev/null
+++ b/dom/smil/test/test_smilWithXlink.xhtml
@@ -0,0 +1,48 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for animate with xlink:href attribute.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <style>
+ div#target {
+ width: 300px;
+ height: 100px;
+ background-color: red;
+ }
+ </style>
+</head>
+<body>
+<p id="display"></p>
+<div id="target"></div>
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ id="svg">
+ <animate xlink:href="#target"
+ attributeName="width" from="0" to="200" dur="10s" fill="freeze"/>
+</svg>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ var svg = document.getElementById("svg");
+ var target = document.getElementById("target");
+
+ svg.pauseAnimations();
+ svg.setCurrentTime(5);
+
+ var cs = getComputedStyle(target);
+ is(cs.width, "100px", "SMIL should affect outer element.");
+
+ SimpleTest.finish();
+ return;
+}
+
+window.addEventListener("load", runTest);
+
+]]>
+</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 0000000000..eb0f84268c
--- /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 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);
+]]>
+</script>
+</pre>
+</body>
+</html>