diff options
Diffstat (limited to 'dom/smil')
167 files changed, 22557 insertions, 0 deletions
diff --git a/dom/smil/SMILBoolType.cpp b/dom/smil/SMILBoolType.cpp new file mode 100644 index 000000000..f6ae19b79 --- /dev/null +++ b/dom/smil/SMILBoolType.cpp @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILBoolType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include <math.h> + +namespace mozilla { + +void +SMILBoolType::Init(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mBool = false; + aValue.mType = this; +} + +void +SMILBoolType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mBool = false; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +SMILBoolType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mBool = aSrc.mU.mBool; + return NS_OK; +} + +bool +SMILBoolType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mBool == aRight.mU.mBool; +} + +nsresult +SMILBoolType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // bool values can't be added to each other +} + +nsresult +SMILBoolType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between bool values +} + +nsresult +SMILBoolType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // bool values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILBoolType.h b/dom/smil/SMILBoolType.h new file mode 100644 index 000000000..608a09ccf --- /dev/null +++ b/dom/smil/SMILBoolType.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_SMILBOOLTYPE_H_ +#define MOZILLA_SMILBOOLTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +namespace mozilla { + +class SMILBoolType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static SMILBoolType* Singleton() + { + static SMILBoolType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILBoolType() {} +}; + +} // namespace mozilla + +#endif // MOZILLA_SMILBOOLTYPE_H_ diff --git a/dom/smil/SMILEnumType.cpp b/dom/smil/SMILEnumType.cpp new file mode 100644 index 000000000..2aa7a04c1 --- /dev/null +++ b/dom/smil/SMILEnumType.cpp @@ -0,0 +1,83 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILEnumType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include <math.h> + +namespace mozilla { + +void +SMILEnumType::Init(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mUint = 0; + aValue.mType = this; +} + +void +SMILEnumType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mUint = 0; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +SMILEnumType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mUint = aSrc.mU.mUint; + return NS_OK; +} + +bool +SMILEnumType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mUint == aRight.mU.mUint; +} + +nsresult +SMILEnumType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // enum values can't be added to each other +} + +nsresult +SMILEnumType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between enum values +} + +nsresult +SMILEnumType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // enum values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILEnumType.h b/dom/smil/SMILEnumType.h new file mode 100644 index 000000000..b6cda3ff9 --- /dev/null +++ b/dom/smil/SMILEnumType.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_SMILENUMTYPE_H_ +#define MOZILLA_SMILENUMTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +namespace mozilla { + +class SMILEnumType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static SMILEnumType* + Singleton() + { + static SMILEnumType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILEnumType() {} +}; + +} // namespace mozilla + +#endif // MOZILLA_SMILENUMTYPE_H_ diff --git a/dom/smil/SMILIntegerType.cpp b/dom/smil/SMILIntegerType.cpp new file mode 100644 index 000000000..194653e1b --- /dev/null +++ b/dom/smil/SMILIntegerType.cpp @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILIntegerType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include <math.h> + +namespace mozilla { + +void +SMILIntegerType::Init(nsSMILValue& aValue) const +{ + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mInt = 0; + aValue.mType = this; +} + +void +SMILIntegerType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mInt = 0; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +SMILIntegerType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mInt = aSrc.mU.mInt; + return NS_OK; +} + +bool +SMILIntegerType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mInt == aRight.mU.mInt; +} + +nsresult +SMILIntegerType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + aDest.mU.mInt += aValueToAdd.mU.mInt * aCount; + return NS_OK; +} + +nsresult +SMILIntegerType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + aDistance = fabs(double(aTo.mU.mInt - aFrom.mU.mInt)); + return NS_OK; +} + +nsresult +SMILIntegerType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + + const double startVal = double(aStartVal.mU.mInt); + const double endVal = double(aEndVal.mU.mInt); + const double currentVal = startVal + (endVal - startVal) * aUnitDistance; + + // When currentVal is exactly midway between its two nearest integers, we + // jump to the "next" integer to provide simple, easy to remember and + // consistent behaviour (from the SMIL author's point of view). + + if (startVal < endVal) { + aResult.mU.mInt = int64_t(floor(currentVal + 0.5)); // round mid up + } else { + aResult.mU.mInt = int64_t(ceil(currentVal - 0.5)); // round mid down + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/smil/SMILIntegerType.h b/dom/smil/SMILIntegerType.h new file mode 100644 index 000000000..39560cc79 --- /dev/null +++ b/dom/smil/SMILIntegerType.h @@ -0,0 +1,46 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_SMILINTEGERTYPE_H_ +#define MOZILLA_SMILINTEGERTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +namespace mozilla { + +class SMILIntegerType : public nsISMILType +{ +public: + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + + static SMILIntegerType* + Singleton() + { + static SMILIntegerType sSingleton; + return &sSingleton; + } + +private: + constexpr SMILIntegerType() {} +}; + +} // namespace mozilla + +#endif // MOZILLA_SMILINTEGERTYPE_H_ diff --git a/dom/smil/SMILStringType.cpp b/dom/smil/SMILStringType.cpp new file mode 100644 index 000000000..d67323b7e --- /dev/null +++ b/dom/smil/SMILStringType.cpp @@ -0,0 +1,91 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILStringType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include "nsString.h" + +namespace mozilla { + +void +SMILStringType::Init(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mPtr = new nsString(); + aValue.mType = this; +} + +void +SMILStringType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + delete static_cast<nsAString*>(aValue.mU.mPtr); + aValue.mU.mPtr = nullptr; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +SMILStringType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + + const nsAString* src = static_cast<const nsAString*>(aSrc.mU.mPtr); + nsAString* dst = static_cast<nsAString*>(aDest.mU.mPtr); + *dst = *src; + return NS_OK; +} + +bool +SMILStringType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + const nsAString* leftString = + static_cast<const nsAString*>(aLeft.mU.mPtr); + const nsAString* rightString = + static_cast<nsAString*>(aRight.mU.mPtr); + return *leftString == *rightString; +} + +nsresult +SMILStringType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // string values can't be added to each other +} + +nsresult +SMILStringType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between string values +} + +nsresult +SMILStringType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // string values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILStringType.h b/dom/smil/SMILStringType.h new file mode 100644 index 000000000..6f160cadb --- /dev/null +++ b/dom/smil/SMILStringType.h @@ -0,0 +1,51 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef MOZILLA_SMILSTRINGTYPE_H_ +#define MOZILLA_SMILSTRINGTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +namespace mozilla { + +class SMILStringType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static SMILStringType* + Singleton() + { + static SMILStringType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILStringType() {} +}; + +} // namespace mozilla + +#endif // MOZILLA_SMILSTRINGTYPE_H_ diff --git a/dom/smil/TimeEvent.cpp b/dom/smil/TimeEvent.cpp new file mode 100644 index 000000000..8a58c6750 --- /dev/null +++ b/dom/smil/TimeEvent.cpp @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ContentEvents.h" +#include "mozilla/dom/TimeEvent.h" +#include "nsIDocShell.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsPresContext.h" +#include "nsGlobalWindow.h" + +namespace mozilla { +namespace dom { + +TimeEvent::TimeEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent) + : Event(aOwner, aPresContext, + aEvent ? aEvent : new InternalSMILTimeEvent(false, eVoidEvent)) + , mDetail(mEvent->AsSMILTimeEvent()->mDetail) +{ + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } + + if (mPresContext) { + nsCOMPtr<nsIDocShell> docShell = mPresContext->GetDocShell(); + if (docShell) { + mView = docShell->GetWindow(); + } + } +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(TimeEvent, Event, + mView) + +NS_IMPL_ADDREF_INHERITED(TimeEvent, Event) +NS_IMPL_RELEASE_INHERITED(TimeEvent, Event) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TimeEvent) + NS_INTERFACE_MAP_ENTRY(nsIDOMTimeEvent) +NS_INTERFACE_MAP_END_INHERITING(Event) + +NS_IMETHODIMP +TimeEvent::GetDetail(int32_t* aDetail) +{ + *aDetail = mDetail; + return NS_OK; +} + +void +TimeEvent::InitTimeEvent(const nsAString& aType, nsGlobalWindow* aView, + int32_t aDetail) +{ + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Event::InitEvent(aType, false /*doesn't bubble*/, false /*can't cancel*/); + mDetail = aDetail; + mView = aView ? aView->GetOuterWindow() : nullptr; +} + +} // namespace dom +} // namespace mozilla + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<TimeEvent> +NS_NewDOMTimeEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent) +{ + RefPtr<TimeEvent> it = new TimeEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/smil/TimeEvent.h b/dom/smil/TimeEvent.h new file mode 100644 index 000000000..b5af5747e --- /dev/null +++ b/dom/smil/TimeEvent.h @@ -0,0 +1,71 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef mozilla_dom_TimeEvent_h_ +#define mozilla_dom_TimeEvent_h_ + +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TimeEventBinding.h" +#include "nsIDOMTimeEvent.h" + +class nsGlobalWindow; + +namespace mozilla { +namespace dom { + +class TimeEvent final : public Event, + public nsIDOMTimeEvent +{ +public: + TimeEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent); + + // nsISupports interface: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TimeEvent, Event) + + // nsIDOMTimeEvent interface: + NS_DECL_NSIDOMTIMEEVENT + + // Forward to base class + NS_FORWARD_TO_EVENT + + virtual JSObject* WrapObjectInternal(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override + { + return TimeEventBinding::Wrap(aCx, this, aGivenProto); + } + + void InitTimeEvent(const nsAString& aType, nsGlobalWindow* aView, + int32_t aDetail); + + + int32_t Detail() const + { + return mDetail; + } + + nsPIDOMWindowOuter* GetView() const + { + return mView; + } + +private: + ~TimeEvent() {} + + nsCOMPtr<nsPIDOMWindowOuter> mView; + int32_t mDetail; +}; + +} // namespace dom +} // namespace mozilla + +already_AddRefed<mozilla::dom::TimeEvent> +NS_NewDOMTimeEvent(mozilla::dom::EventTarget* aOwner, + nsPresContext* aPresContext, + mozilla::InternalSMILTimeEvent* aEvent); + +#endif // mozilla_dom_TimeEvent_h_ diff --git a/dom/smil/crashtests/1010681-1.svg b/dom/smil/crashtests/1010681-1.svg new file mode 100644 index 000000000..5dd394472 --- /dev/null +++ b/dom/smil/crashtests/1010681-1.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> +<script> +<![CDATA[ + +function boom() +{ + var animate = + document.createElementNS("http://www.w3.org/2000/svg", "animate"); + animate.setAttribute("dur", "2s"); + document.documentElement.appendChild(animate); + animate.targetElement; + animate.requiredFeatures.insertItemBefore(0, 0); + document.documentElement.setCurrentTime(4); + document.documentElement.setCurrentTime(0); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +]]> +</script></svg> diff --git a/dom/smil/crashtests/483584-1.svg b/dom/smil/crashtests/483584-1.svg new file mode 100644 index 000000000..b9ded113e --- /dev/null +++ b/dom/smil/crashtests/483584-1.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <g id="s"> + <circle/> + <animateTransform attributeName="transform"/> + </g> + <use xlink:href="#s"/> +</svg> diff --git a/dom/smil/crashtests/483584-2.svg b/dom/smil/crashtests/483584-2.svg new file mode 100644 index 000000000..f5cbd7d46 --- /dev/null +++ b/dom/smil/crashtests/483584-2.svg @@ -0,0 +1,133 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- =====================================================================--> +<!-- animate-elem-30-t.svg --> +<!-- --> +<!-- Tests various types of animations on referenced elements. --> +<!-- --> +<!-- Author : Ola Andersson, 22-Sep-2003 --> +<!--======================================================================--> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="tiny" id="svg-root" width="480" height="360" viewBox="0 0 480 360" onload="go()" class="reftest-wait"> + <script> + function go() { + var svg = document.documentElement; + svg.pauseAnimations(); + + // Note: Animations in this testcase have begin="100" dur="3". + + // Jump to partway through animation... + svg.setCurrentTime(102); + + // ...and then (if we didn't hang) jump back to a pre-animation time. + svg.setCurrentTime(50); + + // Signal that the test is complete: + svg.removeAttribute("class"); + } + </script> + <g transform="translate(20) scale(1.3)"> + <!-- SILHOUETTES--> + <path d="M210 40 C210 40 210 100 170 190" fill="none" stroke="#b4b4b4"/> + <path d="M 171 188 l 10 -10 l -10 -4 z" fill="#b4b4b4" stroke="none"/> + <polyline fill="none" stroke="#b4b4b4" stroke-width="2" points="200,20 200,40 220,40 220,60"/> + <polyline transform="rotate(15)" fill="none" stroke="#b4b4b4" stroke-width="9" points="200,120 200,140 220,140 220,160"/> + + <line x1="40" y1="50" x2="20" y2="10" stroke="#b4b4b4" stroke-width="3"/> + <line x1="160" y1="50" x2="80" y2="10" stroke="#b4b4b4" stroke-width="3"/> + <line x1="30" y1="30" x2="120" y2="30" stroke="#b4b4b4"/> + <path d="M 120 30 l -10 3 l 0 -6 z" fill="#b4b4b4" stroke="none"/> + + <line x1="70" y1="70" x2="145" y2="70" stroke="#b4b4b4"/> + <rect x="10" y="60" width="60" height="20" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/> + <rect x="145" y="60" width="30" height="40" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/> + <path d="M 145 70 l -10 3 l 0 -6 z" fill="#b4b4b4" stroke="none"/> + + <circle cx="30" cy="100" r="10" fill="#b4b4b4" stroke="#b4b4b4"/> + <circle cx="100" cy="195" r="15" fill="#b4b4b4" stroke="#b4b4b4"/> + <line x1="37" y1="107" x2="89" y2="184" stroke="#b4b4b4"/> + <path d="M 90 185 l -3 -12 l -6 5 z" fill="#b4b4b4" stroke="none"/> + + <rect x="300" y="10" width="20" height="20" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/> + <rect x="300" y="170" width="20" height="40" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/> + <line x1="310" y1="30" x2="310" y2="170" stroke="#b4b4b4"/> + <path d="M 310 170 l -3 -10 l 6 0 z" fill="#b4b4b4" stroke="none"/> + + <rect x="230" y="7.5" width="40" height="7.5" fill="#b4b4b4" stroke="none"/> + <rect x="230" y="170" width="40" height="30" fill="#b4b4b4" stroke="none"/> + <line x1="250" y1="10" x2="250" y2="170" stroke="#b4b4b4"/> + <path d="M 250 170 l -3 -10 l 6 0 z" fill="#b4b4b4" stroke="none"/> + <!-- END OF SILHOUETTES--> + + <!-- DEFS--> + <defs> + <line id="lineID" x1="30" y1="50" x2="10" y2="10" stroke="rgb(16, 93, 140)" stroke-width="3"> + <animate attributeName="x1" from="30" to="90" begin="100" dur="3" fill="freeze"/> + </line> + </defs> + + <defs> + <rect id="rectID" x="10" y="60" width="60" height="20" fill="blue" stroke="black" stroke-width="2"> + <animateColor attributeName="fill" from="white" to="rgb(16, 93, 140)" begin="100" dur="3" fill="freeze"/> + <animate attributeName="height" from="20" to="40" begin="100" dur="3" fill="freeze"/> + </rect> + </defs> + + <defs> + <circle id="circleID" cx="20" cy="100" r="10" fill="rgb(16, 93, 140)" stroke="black" transform=""> + <animate attributeName="cy" from="100" to="130" begin="100" dur="3" fill="freeze"/> + <animateTransform attributeName="transform" type="scale" from="1" to="1.5" additive="sum" begin="100" dur="3" fill="freeze"/> + </circle> + </defs> + + <defs> + <polyline id="polylineID" fill="none" stroke="rgb(16, 93, 140)" stroke-width="2" points="200,20 200,40 220,40 220,60"> + <animateMotion path="M 0 0 l 0 100" begin="100" dur="3" fill="freeze"/> + <animate attributeName="stroke-width" from="2" to="9" begin="100" dur="3" fill="freeze"/> + </polyline> + </defs> + + <defs> + <polygon id="polygonID" fill="green" stroke="black" points="240,20 240,40 260,40 260,20" stroke-width="2"> + <animate attributeName="fill" from="white" to="rgb(16, 93, 140)" begin="100" dur="3" fill="freeze"/> + </polygon> + </defs> + + <defs> + <image id="imageID" x="230" y="20" width="40" height="80" xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEASABIAAD/4RX+RXhpZgAASUkqAAgAAAAJAA8BAgAGAAAAegAAABABAgAXAAAAgAAAABIBAwABAAAAAQAAABoBBQABAAAAoAAAABsBBQABAAAAqAAAACgBAwABAAAAAgAAADIBAgAUAAAAsAAAABMCAwABAAAAAQAAAGmHBAABAAAAxAAAAGYFAABDYW5vbgBDYW5vbiBESUdJVEFMIElYVVMgMzAwAAAAAAAAAAAAALQAAAABAAAAtAAAAAEAAAAyMDAyOjAxOjE1IDA0OjQyOjU4ABsAmoIFAAEAAABWAwAAnYIFAAEAAABeAwAAAJAHAAQAAAAwMjEwA5ACABQAAAAOAgAABJACABQAAAAiAgAAAZEHAAQAAAABAgMAApEFAAEAAAA+AwAAAZIKAAEAAABGAwAAApIFAAEAAABOAwAABJIKAAEAAABmAwAABZIFAAEAAABuAwAABpIFAAEAAAB2AwAAB5IDAAEAAAAFAAAACZIDAAEAAAAAAAAACpIFAAEAAAB+AwAAfJIHAJoBAACGAwAAhpIHAAgBAAA2AgAAAKAHAAQAAAAwMTAwAaADAAEAAAABAAAAAqADAAEAAABABgAAA6ADAAEAAACwBAAABaAEAAEAAAAwBQAADqIFAAEAAAAgBQAAD6IFAAEAAAAoBQAAEKIDAAEAAAACAAAAF6IDAAEAAAACAAAAAKMHAAEAAAADAAAAAAAAADIwMDI6MDE6MTUgMDQ6NDI6NTgAMjAwMjowMToxNSAwNDo0Mjo1OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAQAAAPUAAAAgAAAA1QAAACAAAAABAAAAyAAAAGQAAAAKAAAAAAAAAAMAAACs3QIAAAABAP//AADoAwAAMAEAACAAAAAMAAEAAwAmAAAAHAQAAAIAAwAEAAAAaAQAAAMAAwAEAAAAcAQAAAQAAwAaAAAAeAQAAAAAAwAGAAAArAQAAAAAAwAEAAAAuAQAAAYAAgAgAAAAwAQAAAcAAgAYAAAA4AQAAAgABAABAAAAcVYQAAkAAgAgAAAA+AQAABAABAABAAAAAAAEAQ0AAwAEAAAAGAUAAAAAAABMAAIAAAADAAEAAAAAAAQAAAABAAAAAAAAAAAAAAAAAAAAAwABAAEwAAD/////BgKtACAAdADVAP//AAAAAAAAAAAAAP//AABABkAGAgAwAdMAngAAAAAAAAAAADQAAACPAD8B1QD1AAAAAAAAAAEAAwAAAAAAAAAHMAAAAAAAAAAA//8AANUA+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElNRzpESUdJVEFMIElYVVMgMzAwIEpQRUcAAAAAAAAARmlybXdhcmUgVmVyc2lvbiAxLjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAPQA9AD0AABqGADOAAAAgE8SAJsAAAAEAAEAAgAEAAAAUjk4AAIABwAEAAAAMDEwMAEQAwABAAAAQAYAAAIQAwABAAAAsAQAAAAAAAAGAAMBAwABAAAABgAAABoBBQABAAAAtAUAABsBBQABAAAAvAUAACgBAwABAAAAAgAAAAECBAABAAAA9AUAAAICBAABAAAA7g8AAAAAAAC0AAAAAQAAALQAAAABAAAA//////////////////////////////////////////////////9//////////////9j/2wCEAAkGBggGBQkIBwgKCQkLDRYPDQwMDRwTFRAWIR0jIiEcIB8kKTQsJCcxJx4fLT0tMTY3Ojo6Iio/RD44QjM3OTYBCQkJDAoMFAwMFA8KCgoPGhoKChoaTxoaGhoaT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT//AABEIAHgAoAMBIQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AOxApcV7J5goUngDNIVweaBC80oYjvRYLseszAY60FjI3c1HLZ3L5r6DjG46jFRFjTVmJtoA5qW3j3vuIBAokuVNgnzOxpKVA5xUNyY3TB4Fc0b3Oh2sZbqAxx0pMV2pnG9xNtJincA20hWmBMBT1XJrJuxaLf7pU4wKqScuayp3vqaTtbQABjmnRJunQdiRVtuz8kQlsa0kUUg+ZFP4VVeyUNuiJB9DXn060oabx6o7Z0oy12l3I5pMDawwcVTIrupbX6PY46u9uq3ExT0kZBxWrV9DNOw4zue9MZi3U0lFIbm2NwKTFUIMUmKYgxSYoAmAFP2+9Z3NLCbaTbzRcmw9baR13KpIp8ETLcpuUjnvWcqsbNXXMk9DSNOV07aX3NInFNPTmvLO8iljWVcN19az2hZZNmMkniuzC1LXg9t0c2Ihe0lv1JZrGWEZIBX1FV8V1U6iqK6+aOacHB2fyYYorQkMUmKYgxSYoEGKTFAyYCnYrM0FCM33VJ+go8mQfwN+VS5xWjaTDlk9kzSRBHGq+gpoO6ZB6mvLk7tvuz0ErK3YlkIQVAz0hkZamhlWRXIztNCdtVuDV9zRDBlBGCCKz7uyO8NCuQeoHatqFTkld/C9zOrDnj5rYYunSn7xVfqaf/Zn/TTn/drpli4p6Lm89jnjhn1dvIo3TRWt5HbGQNNICwUdQB3NGK3pVPaR5tvIyqQ5HbfzDFJitDMMUYoAlxSgVmaGlAAkCjvjNKeTXlzd5N92ehFWSXZCHmoyQjBj2qGUMkn3GojJQA0yUKGf7ozQBbtd6x4fgZ4qxmhMGGcCsjxP4ktvDGiS39yQSPljjzy79hTEcN8P7y51mbUNd1KQGSZxGrHgDP8ACPyFdtivRw1vZ6dHqcNe/P6rQcIXPRGP4UxsLJ5bEB8Z298etbc8b2ur9jLkklezt3DFGKsRLQKzLNFJULiPeofGduecfSiR/wB4qD6mvIb/ADPSQuPWqt+3lxKfVgKT2AqmSk3E9KALUFkW+aTgelWtqhhGgx647Cj+kBI/3Pl7UwOCMijqBICGXNcj448Ax+MfLkN/NbyQqRGmA0fPcj+tMCPwz8PRpWnWcN/ePKYG83yovljL+p7nH4V1sjQWcLSyFIo0GWduAB9apSaVk7J7onlTd+q2Z5n4x+MKWxa08PKJW+610w4B/wBkd/rWr4Es7pdE/tDUpHlvb7EsjyHJ2/wj8j+tb4WN537IyxDtC3dnSYpMV6Jwkt8sktm6wSLFMynY5G7afpXkl3Y61qVyY7zVrkxkncN52kewBx+lediJuKWrSfQ76EVJvRO3Ut6zo0+p3Ed19rc3MUYRC3QADjGOhrofCGvz6bpPk61cyTXab9ryMWLDjA3f56VxKVzqcbGA/wAWdZtrly8CGLcdvm2xAx9Q2f0rc0P4lDxPKbae2t4XjXzN0UrENj/ZZRj86p/CQtzoZryKCJpZpFSNeSxPArk7z4iTC/UaMtkYUJ3SXT43n0AH86Fq7feGxq6Z8VbOWdLfVbY20zEKrwSCaMn8OR+VXbn4laFp9xLDJJcSzLkuIoSQvtk4FNiSuZU3xhtCT9i0i7nHrJIkf9TWx4Y8YxeIIpC8Is5g3+pMofH48UPa4Lsak3iTTbGyuJ7m6RI7Ztsp67T9PxqDTPG2ia7c/ZdMvBNMRu27COO55FF9ANHV9Zs9D097u+lEcSD8WPoBXhPjf4g6j4puWt4w9tYqflhBwW929aa19AMTw9aW93rlnDqEghtjMDIzdMelfREQR4lMJUxkfKV6Y9q7sK1Z9zkxKd12HbKPLrsucyRxsuuXeq25SOUKu8gPESpOPftWMblrWcLJueI/dmHI/E/1rw61R1Ja9NkevSgoR069TQVy6EoVbjiqsy3ZiO+GFXGNoEpIP47axNSgEvPNEF2trI0udqtKRkAc8bKbaaVGkpntbK383JV2W4ZcH0+7zVXttdE2vvZly4tLq8h8q5t4pIyeVN45H/oFVR4cjwB/Zdpjtm6f/wCJpKVurHy+SLFto2m7leK3iEqHBKMTtYe9OGmafcM8hgjkdnIdjzn1pOTGoortoMIkbytNsiueCzkE/wDjpp8FobWbZb2llE57JMQf/QafN5sXL5IfPo8l4jR3SoiH5iEkLZYdCQRin2tzc6FeR3S+XIkYwX+6Rn17VUZW03RMoX12Zj+JdQufEOprcXOr26LH/q4lOQtZc2keYd0uqQMQOpQZquZ9mRZdyGDRx5+WvbfAPH3Rmuy0XXLzw/A/2cG7gJ4QPuVfy71pTquDvYmcFNWbNmx+JFoqyfb4pyxclfLjXCr2H3ucetaVt4+0W4OPOkiP/TRMfrXbHFQe90zklhpLazRyMNxZ21v5K288aHPApIb3TbW0S2iSZUUbQCCePQ15soy6pps7ozj0asgtLnTbIsIXmVDzsIOF+lWDqliwO6duTkEjpUtd0ylLzRQ1KPS9TMZkvZImiyFaNtp5x/hViyfT9Ph8q3uiUzn5jk/nQ2rWBb3Lkd/bYybhT7U4X9sDj7SnHvU6FXZS8i3NvPHHqAjMzM+4EAqSe1Q6fNp+kWotZNTEzK2Q0jYNVvsTe2uyJW16yQEC+gbHvTtN8S6PaXsk1xHFdyOVKlwMIADkA571SjZ9/IHJNb2H3niawu7xpYpEgjzxGqggD65rOnbTjayQx3yqkyjOV3EHGOxH5UW12Fdd9jMtvDmhxXsdw140qoM+U0eVc+/tTNV8PWd/J5kV4kSseALfvgZ6DNac2mxHKu5WPgm23AW+qOwZQDut2Xn04zxnFT2ng9LEiSLWbm3nGdzRWz4Ht2zS5vIfL5mja2chB+33kF1nAz5Tow/HFOm0mFoSI7xAwPyoyEjHfnH9PWpv6jS80dK8cJGdwz9aiFvHnOBXqSVzzouwvkJnBVc/SnizQ/8ALMflXPKKNVJgbCIjmMflUf2C3brEpH+7kVm4ItSGtpdqRzBHj12CoG0mzbAMMeO3FR7MrmFGkWfCmEfhUM3hyykk3FXBPo5oVJBzsrt4QsHck+aPo/8A9aoz4K08E/vJP0P9Kr2fYnnI5PBViQQJZF/Af4VA3gi1B4nbp/cWj2TH7Qhk8GQAACZuneMCq0vhDYMpMufdf/r1SovuJ1UVz4WlLcNH9cH/ABqQeDLhjlZIPx3CmsPJ9SXWS6Cnwhfx/ceL8JmH9KP+Ea1ZD8s34i5Yf0p/Vp+TBYiPmd35eOgIoAJHBH511M50O29C4U47mnhQykBiT6BqxZqhwJTG4N9OtSrtY98+/Ws5eRcfMCgJzj2yKjaMZ64x6VCZTQ5VULyfxNIGRgR5gb2NUri0HeUj5GUcdhjpQzIBhlyvrkD+tUtfInReY37RbO2zcPTOajaKFXz5hI+uaaUkDcWRzRIVzge2BVaXYCBzitY/kZSsvmQmNQc5qSEAg7cH29K0iRIcSGGGcj2yKcANuA/Hck5NWQSzzKR8u/B6hRinIwAA+bH8qybNUSD/AH2A9MnFPDYXA5H51k0aIRkDMA0av6e1SCFduCij6VnJtFJJiGCI9ET64puVU4wc+oBqE2y7JApTdtXeD64OKcx6DJB9s1RIi5ZuCAMdQc0hWRELABiO3JNVdE2ZD+9UBinPcBj/AIUiXEj5DAoMd8mrsmTdr5jHBA+VcH1AzUTKyj69+laRsQ7kZXPynDe2KdHDGP8Alnj8K0RmxzBAOij0HSnxIsikMiqPUNVCHS28RxvJJ+tMEaRP8oOPUtmsWaIsIePf2pySgdT9NwrFo1TFNxs5YoR2waYssLO3mJHk9e+aVn0uO662BhCWyhUevz8flTiM/wCrOf8AgVRfv99i15fdcbmVcMUZiB0UgioJL64QkfZZAPUNVxSe1iJSa6MbHfSsQVtnXuS3FTfanYjfGoxxgfMavl8yObyHl02kLGcn/pkaYJJBn5MA+vGaSXdjv2RCbmYEqVXPs1NeUsvJXPrWkUlsQ5N76EQjycb2+meKsIDj1x6CtIoiV+o/eQvfHuKA/wAvYfhVkjC0gHVQfXFRkXDYO9GI6DFYNmthn+mZz8uP9lQamjmuVA/csffgVDsylddCdLhgp3oE79aie5lxhFWQexJqeW3Urm8vkVvt8gfbIoA/3CDVhJVxlCg+oNJx82ClforoZNdXce0pFGy/3gc5pq307H96kZB7KDQoIHN9kSf2gVOFXr2KkU7+1sAB1z9Oark8xe0EXU4n6RsD9KSd4WQFhz24zTUXHzE5KXkQOY9uMNjsNtIEj2/xfTbjNap+RnZdxu1edq7f6frT0hdlyJG/4C55ql9xLEMjxcBps/gc09LknO/cfZkH+NNaAOUTyjLsY19AaUzJHncWPrmsWro1TsC38J6tge4NONxBMAFc59FJrFprbU1Ti9HoD2sLJhmbHo3SmJZwA5Bj46dqqM7kygiVJkiB+YnPbPFMa9jfcFwSPek43dxqSSsNa7mUbvLVB7tSHUHWLcYmHtkHNCh5sXP5EB1p0TAtHx6g4/lU0OqQyLlrd93q4z+tVZ9G0LnXVCyXdw5/cBMf7uaga+uQwLxIT6gYqlHzZLl5KwNcs7ZljOT6UmYznkp+JFaIhjMRqM+ac/72aljDsR+/z9GqkTYk8uRTnex+rVIszIMfOSfamB//2apR82S5eSsDXLO2ZYzk+lJmM//Y/+0O2lBob3Rvc2hvcCAzLjAAOEJJTQPtClJlc29sdXRpb24AAAAAEABIAAAAAQACAEgAAAABAAI4QklNBA0YRlggR2xvYmFsIExpZ2h0aW5nIEFuZ2xlAAAAAAQAAAAeOEJJTQQZEkZYIEdsb2JhbCBBbHRpdHVkZQAAAAAEAAAAHjhCSU0D8wtQcmludCBGbGFncwAAAAkAAAAAAAAAAAEAOEJJTQQKDkNvcHlyaWdodCBGbGFnAAAAAAEAADhCSU0nEBRKYXBhbmVzZSBQcmludCBGbGFncwAAAAAKAAEAAAAAAAAAAjhCSU0D9RdDb2xvciBIYWxmdG9uZSBTZXR0aW5ncwAAAEgAL2ZmAAEAbGZmAAYAAAAAAAEAL2ZmAAEAoZmaAAYAAAAAAAEAMgAAAAEAWgAAAAYAAAAAAAEANQAAAAEALQAAAAYAAAAAAAE4QklNA/gXQ29sb3IgVHJhbnNmZXIgU2V0dGluZ3MAAABwAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAADhCSU0ECAZHdWlkZXMAAAAAEAAAAAEAAAJAAAACQAAAAAA4QklNBB4NVVJMIG92ZXJyaWRlcwAAAAQAAAAAOEJJTQQaBlNsaWNlcwAAAAB3AAAABgAAAAAAAAAAAAAAWgAAAHgAAAALAG8AcABlAHIAYQBfAGgAbwB1AHMAZQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAeAAAAFoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEJJTQQREUlDQyBVbnRhZ2dlZCBGbGFnAAAAAQEAOEJJTQQUF0xheWVyIElEIEdlbmVyYXRvciBCYXNlAAAABAAAAAE4QklNBAwVTmV3IFdpbmRvd3MgVGh1bWJuYWlsAAALLwAAAAEAAABwAAAAVAAAAVAAAG5AAAALEwAYAAH/2P/gABBKRklGAAECAQBIAEgAAP/uAA5BZG9iZQBkgAAAAAH/2wCEAAwICAgJCAwJCQwRCwoLERUPDAwPFRgTExUTExgRDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBDQsLDQ4NEA4OEBQODg4UFA4ODg4UEQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAFQAcAMBIgACEQEDEQH/3QAEAAf/xAE/AAABBQEBAQEBAQAAAAAAAAADAAECBAUGBwgJCgsBAAEFAQEBAQEBAAAAAAAAAAEAAgMEBQYHCAkKCxAAAQQBAwIEAgUHBggFAwwzAQACEQMEIRIxBUFRYRMicYEyBhSRobFCIyQVUsFiMzRygtFDByWSU/Dh8WNzNRaisoMmRJNUZEXCo3Q2F9JV4mXys4TD03Xj80YnlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3EQACAgECBAQDBAUGBwcGBTUBAAIRAyExEgRBUWFxIhMFMoGRFKGxQiPBUtHwMyRi4XKCkkNTFWNzNPElBhaisoMHJjXC0kSTVKMXZEVVNnRl4vKzhMPTdePzRpSkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2JzdHV2d3h5ent8f/2gAMAwEAAhEDEQA/AOmAU2VPf9ETHJSDURr7GCG6BaJJ6NMeKJzdpjwTahTMkkn5pnTGunx0SvZHdKKcoMDw0lrhIjXRRrrdZu1gjkHlX2yK2DwaPyKFjQ/nQ/vDlVvvGpsDzDY9nQUT5FoPBa6FGXcSiW1uY4h2s8HxTPrex217S13gdFZiYkDY2wGwTvo2an1VMiQh5F1VjdsSeyBCUICAu7KuM1VMITQiEJoT7WMNqbapwlCNqf/Q6sBTZW95hgkjUplZxPovI5JA+5Xck+GJLVhHikAiZjWCxu8DbMnVWbX+09/xTmAQO5/ImcNCfAKpPIZ1daNiEBG66snlrWD4D8iC53igesXAGeQITbydBr5JlrkhcJDiNxaQ4D4K48V31gO9zHag9xPcKqzGfAdZpOjWjklHawVAMBkfx5KVnTwVQa4wXk+54A7dynODW1pc63Y1oLnvdDWtaBue97ifaxjfpK0NeFx3+Md/1it6fX03peHc/DySPtmXUN8ydteJtq3W11bvfc9zP0v80pffyfvfgFnsw7Ovi5VGZQ3KxiXY9hd6NhEb2NcWNu2/mtt272fyEWFHpODlMxG0WUfZKMcMpxhbHqvrYxodfbSwv9H1Ld/p1P8A0npfzv6RR631PovQMcX9UyjW5wmrHrAddZH+ipn/AMEs2VKwM8BEXLWtdGA4ZWaGl90kJQhYN1uVh05N1P2Z17fUFBO5zGO91LbXQz9N6Wx9vt9n82jwpQbAPdjqi//R6D6xX9Qx+n+r0p9NWRuaHeqN52nT9CyW/pGrnas3602YRZZ1U15DrRcLamBpDWtLPs3t/wAE/wDnLPalaMzJqpdl0323k7R7txpDnH9J6jj9Gpuz6KakZlVjab632jtkMaQyR+bcyT6Lv/An/mJZcspHSxFOPGIjWiXSwfrP1LG6Re/qbftnWatwoZWw7Lmy30f5oNbvb+k9T+bVKr/GcW2DG6p0v7K+0hm8Pe0N3e3dtsbY13P76Fdhm1hrc/IIJgu0a4T+axzam+z+V/4IqdJtN/2V1GW2qpz2G+xrHVbBP2d9T3U/ztm36DvoKMSOui4xHd62/qfT8PGORk5NdWPUIdYXAzHthjQdz3Pj2LGx/wDGNg02usdjUuxjGwjIAytv77qXsON7/wDRttVduIwmRZYSNTLav/edK2Mf03tNl++xtRa4MhrX+19x9Khrv0aQl4JMPF23/wCMD6tsZRkOdkuGRW6yqsVDeGjcLH2TZsb/ADbm/SVGz/Gf0h5Apw7yyRNll1LC3X6fpNdY93/FrPx8pt2TkUV1PaKNgNjmQHEn3Gt30XMYpWFja33C+6QC8MYWdtdjG+kXJcfgrg8Xrx1/o7Ayx+ZWGW1WXNM/mVAWW7o+g/Z9Gt300uk/WPpfV7jX019zrGNFjvUpfUdjtGmsv/nNzv3Fy13S3041Vue4ltx/mTZVc2RB22Nqr/N3LPtycnp4fZgMymWAij7NW4hz2Wbv0mM/32VtRBO1I4eoL0H1w+u/7GZZhdJr+09Qb7brtu6nGJ/NsI9luV/wX0Kv8KvOul+n1P6wY9vX8hxquuD8vJvl5cGje2l+jttdzw2r9yqtWG4+O3Edl2dKvrq3loabbd5MbnvcyPo7js9T8+z/AItV2ZWDW8XO6blBp9zQXOLYafcR6gc16cCQQaWEWKt9iIL/AHghweNwc2C0g67mub7dqbYV5vifWTIxqaqsa+7Bqa8PrxcmpzWF8+r7bqf55tn+Eru9Nlq1/wDnn1+gzk4+PY0Qfax7ZB/PZayx9StDmY9QQ1zy8uhBf//SC/pmKPoOIJ7Ne7+9MOnVNH85aB4ixytuDWHbMHwCeAAZPPYj+KsSxw/dH2NeM5dz9rXHTTGl14/tlI4FjSP1rI8hvn8FbDARMRroSf4Byk3cCQWkjsY5CiOOPYMgnLuWj9iuDdMy6PEkEf8AUqIx8sGRnWgdx7T/AN9WkS3ktOneFGAHTB1TeCPZdxy7uZkYXUrdhZ1S1gHGgjy42qpZ0jrTy6OruI7BzT+UPW+Tydp2nvIj/NTzXzHuHiATHyThCKDOXd5odE6wyNnUmeGrXAfDlMenfWMBv+UK3bRDY3cf5hXSOuLCP0YduMl2g/6MlRc2ppLgzbPYnUf2DuR9sI9yXd5l+P8AWVp3DMa4wRydPL3M9qGLPrQ1wAu3RwHODm/9L81dHZsk7WwSBogOaOQ2T3AMpwwx8VpzScYH63kfzm4Dgeo0/wDVOSFv1tYI2jaeRLII/lAXLcZu2CA4eIOhUgNCdr4/OiSnfd4eP4f96t+8T8H/05PZkB28OboPzi7/AL6p1OJEz8Cf9qC4vBEPY13jAJj71HZlfmPZH9Uz+VWZFrC240tPmR2EGfkkHMaQBo4nUOEx+IVXfcwAWmrXncQD9wCT8qn6IDNwGo2kg/cmEWuum4TYCI2OB8AZ/wCqc1DLniR7te7SQNP5YZ7VUD73O2iljmd/T0Mf5zXf9FGDrhpWAHRoHgx83Nc5NqlwNsn5uMyA+yHd9Hc+b9j0qs3FfoHbxoZa0kT/AJqGLuonSxrXDuGz/wBUXKX2jJA1phvYOg/IJwH8rWkn+QZzjuJdIYXcgkA6eA9Ru1QYamglu93jALv836X/AFSTb8h3/aQx5QP+qTWixxaHVWb+4B0H4ohHl+SxdU7hrpI5cDz/AGoQvdOjRH3H7lElu73bh81DfXJDXAHydJ/KpAsJbQIiCSPiRP4p2BocXAy7xMfwQCS0Ai+Y/N3NEfDcFJj73HVu8eHt/wC+7U+1r//UGz0tvs/nO+6N0/20x+3bvZt2x7d0fjuXl6SsS2aw3/g+qV/bJHq7Nvlz89ym7kT6cfPn+wvKElGN/wDvWQ7f98+rH7Rr6e3nSP8AzJB/WIHr7v8Arcx5bvzti8vSTvs/7pb9v7H039N6b9vq+XO3+xv/ADkAftLc3Z9q/wCjt+Xqe1ecpIlH2vp1vrbB9t3bZ1mN3/R/QquPT3D0/V2eUxH/AFK85STo/RbL6/V9Lb638rbP50Qmd6kGfS51mV5qknrX0+v0tv5v9nd/FPp+bO3vG3/0Z7l5ekih/9kAOEJJTQQhGlZlcnNpb24gY29tcGF0aWJpbGl0eSBpbmZvAAAAAFUAAAABAQAAAA8AQQBkAG8AYgBlACAAUABoAG8AdABvAHMAaABvAHAAAAATAEEAZABvAGIAZQAgAFAAaABvAHQAbwBzAGgAbwBwACAANgAuADAAAAABADhCSU0EBgxKUEVHIFF1YWxpdHkAAAAAB//+AAAAAQEA/+4ADkFkb2JlAGSAAAAAAf/bAIQAFBERGhIaKRgYKTMnICczJxwcHBwnIhcXFxcXIhEMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAEVGhohHSEiGBgiFA4ODhQUDg4ODhQRDAwMDAwREQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM/8AAEQgAWgB4AwEiAAIRAQMRAf/dAAQACP/EARsAAAMBAQEBAQEBAQEAAAAAAAEAAgMEBQYHCAkKCwEBAQEBAQEBAQEBAQEAAAAAAAECAwQFBgcICQoLEAACAgEDAgMEBwYDAwYCATUBAAIRAyESMQRBUSITYXEygZGxQqEF0cEU8FIjcjNi4YLxQzSSorIV0lMkc8JjBoOT4vKjRFRkJTVFFiZ0NlVls4TD03Xj80aUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm9hEAAgIABQEGBgEDAQMFAwYvAAERAiEDMUESUWFxgZEiEzLwobEEwdHh8UJSI2JyFJIzgkMkorI0U0Rjc8LSg5OjVOLyBRUlBhYmNWRFVTZ0ZbOEw9N14/NGlKSFtJXE1OT0pbXF1eX1VmZ2hv/aAAwDAQACEQMRAD8A9dIiTwoDYJi+o85BjSGibVoDGMjqFN3RdsOgbIvl5O8M6Kso5pRIYt2yRPycqd1cow8CoanV33gPMrWpCcFZCJcOdNIaiApFNUtNITSppUD/0PaSrpiGtvpbhScEpcGdF32gCmigvG1pOqrAwAAQSyZVoxucGiyWsVAEON3w6RgQbUgGTF3j9DIwyL0WrtXaRjijlnD04mUiAByw+L1f4kOt6uOCEqwYzuyS+zOWP/31ifcx/wA2InEGiLelbTqZtWNCVay7cETPJIRiO8izCQnESHBFu009DDTQqmlaQ//R9bqs8elh6kgSP8IeDH+MmUZShilYraJfbXP1ks8KiAI9/wDE82OYlpwfB1a+yJWu7PV6L8Tj1tjaYGHxCTcfxXpJGvUA/qEovjysWQDfjfxPBLpISu47SNfJJ5ybg+p9SMyZRIMTwQ55epxYK9WQjfD5GCWXBAQjZA/i2f8AbcJdNOUjMynuPe4KRB9Thy4px3YpCQ8Ync2ckMY88gP6jtfk49NGN5BM2QYk2xL8Ph2s1/T/AO/VIg+rx9RjykjHKMq/gludgbflMEJdLLdCwT7YNZo5swAlI7QTLbu/i/8AKmFSIPo/2TD6nqbI7/4trxfif4xi/Dxt+LKeIfw/+PfNH4hk6bp/Rx0JjieSXwx/9+PiT6eWQ7pGJJ5lu+JskOnpZZvxjqonMbjHzyj9iMY/7v8A8yP1z8b0ccvTz3wkIn2S+J9+P4vGNDLEg/bl/wDAnrSyRztVs9OleWH4n009BOv6gVevJdTnxZ//0sSCAImAoeBWRMzZhqOPM9BJPKu3RGFdmXqS7wP0uOSEMkt08crHte0Jq3PE1yOYZa+zJP7QB9mX0PTtZMXPEvI4JjHKO0idXu0QevgNdsg+iIrsbxHI82H4nCE9+3cRp5wjJ+JY8hJmDR+yH0vT91olgHgD8m8ScjxcuTps/wAYkaG0fv6TOWXTZQBUogaAD/40+ueniR8I+hyl00DzEfQ3iycjyPR6Q95gfv8A+UnaEsGPSE5iP8JO+P8A7rez9kx3rEN/smH+FvBk5o5Tl6aX2iFen9ixfw/eVXtsvNH/06J1oNA+DBjE9liaezOKNBomrLIl4JJLk0VXtQdGdDoQgHwclLGq34MkS7EfNz/mX2+9pDY252Y66tDepvwpoBd6uZtJmfH7mTO3SMsFW1TAIDYmPEOkZDEX3VIKtB//1J0lwLX04ns19l55c93qzkb+mBxakyHA+lxj/qeg/P5MGBnsMvY3t2jU2svm49+6YRcowJ5+9aA03UiXbhwyc9vmgdIG74ZLu2nWX3PHj57fJ6u3b/V8TQEzB+0iz73Lx4SP3pqMlX4hom+AQx37uw4/N0QmMfaqnnsqB//Z"> + <animate attributeName="y" from="5" to="145" begin="100" dur="3" fill="freeze"/> + </image> + </defs> + <!-- END OF DEFS--> + + <!-- ACTUAL TEST CONTENT--> + <use xlink:href="#lineID"> + <animate attributeName="x" from="10" to="70" begin="100" dur="3" fill="freeze"/> + </use> + + <use xlink:href="#rectID" transform=""> + <animateTransform attributeName="transform" type="translate" from="0 0" to="140 0" begin="100" dur="3" fill="freeze"/> + <animateTransform attributeName="transform" type="scale" from="1 1" to="0.5 1" begin="100" dur="3" additive="sum" fill="freeze"/> + </use> + + <use xlink:href="#circleID"> + <animate attributeName="x" from="10" to="70" begin="100" dur="3" fill="freeze"/> + </use> + + <use xlink:href="#polylineID" transform=""> + <animateTransform attributeName="transform" type="rotate" from="0" to="15" additive="sum" begin="100" dur="3" fill="freeze"/> + </use> + + <use x="60" y="-10" xlink:href="#polygonID" transform=""> + <animateMotion path="M 0 0 l 0 150" begin="100" dur="3" fill="freeze"/> + <animateTransform attributeName="transform" type="scale" from="1 1" to="1 2" begin="100" dur="3" additive="sum" fill="freeze"/> + </use> + + <use xlink:href="#imageID" transform=""> + <animateTransform attributeName="transform" type="scale" from="1 .25" to="1 1" begin="100" dur="3" additive="sum" fill="freeze"/> + </use> + <!-- END OF ACTUAL TEST CONTENT--> + </g> + + <text id="revision" x="10" y="340" font-size="40" stroke="none" fill="black">$Revision: 1.6 $</text> + <rect id="test-frame" x="1" y="1" width="478" height="358" fill="none" stroke="#000000"/> +</svg> diff --git a/dom/smil/crashtests/523188-1.svg b/dom/smil/crashtests/523188-1.svg new file mode 100644 index 000000000..c03cea492 --- /dev/null +++ b/dom/smil/crashtests/523188-1.svg @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait" + onload="setTimeout(removeNode, 0)"> + <script> + function removeNode() { + var node = document.getElementById("myRect"); + node.parentNode.removeChild(node); + document.documentElement.removeAttribute("class"); + } + </script> + <rect id="myRect" x="20" y="20" height="50" width="50" stroke="blue"> + <animate attributeName="stroke-width" from="1" to="9" begin="0s" dur="2s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/525099-1.svg b/dom/smil/crashtests/525099-1.svg new file mode 100644 index 000000000..8eed11489 --- /dev/null +++ b/dom/smil/crashtests/525099-1.svg @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <rect x="20" y="20" height="50" width="50" fill="blue"> + <animate attributeName="display" by="inline" + begin="0s" dur="1s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/526536-1.svg b/dom/smil/crashtests/526536-1.svg new file mode 100644 index 000000000..4fcf35d08 --- /dev/null +++ b/dom/smil/crashtests/526536-1.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait" + onload="setTimeout('boom()', 0)"> +<script type="text/javascript"> +<![CDATA[ +function boom() +{ + document.getElementById("anim").setAttribute("fill", "freeze"); + document.documentElement.removeAttribute("class"); +} +]]> +</script> + <g transform="translate(50 50)"> + <circle r="40" style="fill: yellow; stroke: black; stroke-width: 1"> + <animate id="anim" attributeName="cx" attributeType="XML" + values="0; 200" dur="2s" begin="-1s" repeatCount="0.5"/> + </circle> + </g> +</svg> diff --git a/dom/smil/crashtests/526875-1.svg b/dom/smil/crashtests/526875-1.svg new file mode 100644 index 000000000..281454bf6 --- /dev/null +++ b/dom/smil/crashtests/526875-1.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <animate attributeName="fill-opacity" by="-1"/> +</svg> diff --git a/dom/smil/crashtests/526875-2.svg b/dom/smil/crashtests/526875-2.svg new file mode 100644 index 000000000..73c229da5 --- /dev/null +++ b/dom/smil/crashtests/526875-2.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <animate attributeName="fill-opacity" by="1"/> +</svg> diff --git a/dom/smil/crashtests/529387-1-helper.svg b/dom/smil/crashtests/529387-1-helper.svg new file mode 100644 index 000000000..7885ab71f --- /dev/null +++ b/dom/smil/crashtests/529387-1-helper.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <text y="20pt">abc + <animate attributeName="opacity" from="1" to="0" begin="0s" dur="2s"/> + </text> +</svg> diff --git a/dom/smil/crashtests/529387-1.xhtml b/dom/smil/crashtests/529387-1.xhtml new file mode 100644 index 000000000..de3dbec34 --- /dev/null +++ b/dom/smil/crashtests/529387-1.xhtml @@ -0,0 +1,7 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + <script> + var p = new XMLHttpRequest(); + p.open("GET", "529387-1-helper.svg", false); + p.send(); + </script> +</html> diff --git a/dom/smil/crashtests/531550-1.svg b/dom/smil/crashtests/531550-1.svg new file mode 100644 index 000000000..306f41702 --- /dev/null +++ b/dom/smil/crashtests/531550-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <g><animateTransform attributeName="transform" by="1"/></g> +</svg> diff --git a/dom/smil/crashtests/537157-1.svg b/dom/smil/crashtests/537157-1.svg new file mode 100644 index 000000000..df615ab9a --- /dev/null +++ b/dom/smil/crashtests/537157-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" style="-moz-binding:url(#xbl)"> +<bindings xmlns="http://www.mozilla.org/xbl"> +<binding id="xbl" inheritstyle="false"> +<content> +<svg xmlns="http://www.w3.org/2000/svg"> +<animate attributeName="font-size"/> +</svg> +</content> +</binding> +</bindings> +</svg> diff --git a/dom/smil/crashtests/541297-1.svg b/dom/smil/crashtests/541297-1.svg new file mode 100644 index 000000000..4268232ba --- /dev/null +++ b/dom/smil/crashtests/541297-1.svg @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"><svg id="w"><animate/></svg><script type="text/javascript"> +<![CDATA[ + +function boom() +{ + anim = document.createElementNS("http://www.w3.org/2000/svg", "animate"); + document.documentElement.appendChild(anim); + document.documentElement.removeChild(anim); + + setTimeout(t, 0); + + function t() + { + document.getElementById("w").appendChild(anim); + } +} + +window.addEventListener("load", boom, false); + +]]> +</script></svg> diff --git a/dom/smil/crashtests/547333-1.svg b/dom/smil/crashtests/547333-1.svg new file mode 100644 index 000000000..bac629b49 --- /dev/null +++ b/dom/smil/crashtests/547333-1.svg @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + document.getElementsByTagName("animate")[0].setAttributeNS(null, "attributeName", "font-size"); + document.getElementsByTagName("text")[0].setAttributeNS(null, "fill", "green"); + document.documentElement.removeAttributeNS(null, "x"); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +<text>abc<animate/></text> + +</svg> diff --git a/dom/smil/crashtests/548899-1.svg b/dom/smil/crashtests/548899-1.svg new file mode 100644 index 000000000..c12ed2745 --- /dev/null +++ b/dom/smil/crashtests/548899-1.svg @@ -0,0 +1,14 @@ +<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <circle id="circleID" cx="20" cy="100" r="10" fill="orange" stroke="black">
+ <animateTransform attributeName="transform" type="scale"
+ from="1" to="2" begin="0" dur="3"/>
+ </circle>
+ <rect id="rectID" fill="green" stroke="black" height="100" width="100">
+ <animate attributeName="fill" from="white" to="blue" begin="0" dur="3"/>
+ </rect>
+ </defs>
+ <use xlink:href="#circleID"/>
+</svg>
diff --git a/dom/smil/crashtests/551620-1.svg b/dom/smil/crashtests/551620-1.svg new file mode 100644 index 000000000..2ea83e9c2 --- /dev/null +++ b/dom/smil/crashtests/551620-1.svg @@ -0,0 +1,21 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<animate id="x" begin="y.end"/> +<animate id="y"/> + +<script> + +function boom() +{ + var x = document.getElementById("x"); + var y = document.getElementById("y"); + y.appendChild(x); + y.setAttributeNS(null, "dur", "0.5s"); + y.removeAttributeNS(null, "dur"); +} + +window.addEventListener("load", boom, false); + +</script> + +</svg> diff --git a/dom/smil/crashtests/554141-1.svg b/dom/smil/crashtests/554141-1.svg new file mode 100644 index 000000000..61ce419f5 --- /dev/null +++ b/dom/smil/crashtests/554141-1.svg @@ -0,0 +1,12 @@ +<svg xmlns="http://www.w3.org/2000/svg" + onload=" + document.documentElement.pauseAnimations(); + document.documentElement.setCurrentTime(0); + document.getElementById('b').removeAttribute('begin'); + document.getElementById('a').setAttribute('dur', '1s')"> + <rect> + <animate attributeName="y" attributeType="XML" id="a"/> + <animate attributeName="fill" attributeType="CSS" id="b" + begin="a.end" dur="2s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/554202-1.svg b/dom/smil/crashtests/554202-1.svg new file mode 100644 index 000000000..f3d692ca0 --- /dev/null +++ b/dom/smil/crashtests/554202-1.svg @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"> + +<bindings xmlns="http://www.mozilla.org/xbl"> + <binding id="foo"> + <content> + <animate xmlns="http://www.w3.org/2000/svg" begin="a.begin"> + <children xmlns="http://www.mozilla.org/xbl"/> + </animate> + </content> + </binding> +</bindings> + +<animate id="a" begin="b.begin; 3s"/> +<animate id="b" begin="a.begin"/> + +<script type="text/javascript"> + +function boom() +{ + document.documentElement.pauseAnimations(); + document.documentElement.setCurrentTime(0); + document.getElementById('a').beginElementAt(1); + document.documentElement.style.MozBinding = 'url(#foo)'; +} + +window.addEventListener("load", boom, false); + +</script> + +</svg> diff --git a/dom/smil/crashtests/554202-2.svg b/dom/smil/crashtests/554202-2.svg new file mode 100644 index 000000000..a3bbb3195 --- /dev/null +++ b/dom/smil/crashtests/554202-2.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" + onload=" + document.documentElement.pauseAnimations(); + document.documentElement.setCurrentTime(0); + document.getElementById('a').beginElementAt(1); + document.documentElement.setCurrentTime(2)"> + <!-- + This test case sets up a cycle between simultaneous instance times such that + when the instance times are sorted if this cycle is not detected we will + crash. + --> + <rect width="100" height="100" fill="red"> + <set attributeName="fill" to="blue" begin="a.begin" dur="4s"/> + <set attributeName="fill" to="green" id="a" + begin="b.begin; 3s" dur="4s"/> + <set attributeName="fill" to="red" id="b" + begin="a.begin" dur="4s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/555026-1.svg b/dom/smil/crashtests/555026-1.svg new file mode 100644 index 000000000..76b4cf075 --- /dev/null +++ b/dom/smil/crashtests/555026-1.svg @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait" + onload="go()"> + <script> + function go() { + // setCurrentTime to force a sample + document.documentElement.setCurrentTime(1); + document.documentElement.removeAttribute("class"); + } + </script> + <rect id="myRect" fill="blue" height="40" width="40"> + <!-- The "keyTimes" values below are invalid, but they should be ignored + (and definitely shouldn't trigger any assertion failures) since we're + in paced calcMode. --> + <animate attributeName="x" by="50" calcMode="paced" dur="2s" + keyTimes="0; -1"/> + <animate attributeName="x" by="50" calcMode="paced" dur="2s" + keyTimes=""/> + <animate attributeName="x" by="50" calcMode="paced" dur="2s" + keyTimes="abc"/> + <animate attributeName="x" by="50" calcMode="paced" dur="2s" + keyTimes="5"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/556841-1.svg b/dom/smil/crashtests/556841-1.svg new file mode 100644 index 000000000..92712deaa --- /dev/null +++ b/dom/smil/crashtests/556841-1.svg @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait" + onload="go()"> + <script> + function go() { + // setCurrentTime to force a sample + document.documentElement.setCurrentTime(2); + document.documentElement.removeAttribute("class"); + } + </script> + <rect fill="teal" x="50" y="50" width="20" height="20"> + <animateTransform attributeName="transform" type="rotate" by="30" + calcMode="paced" dur="4s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/572938-1.svg b/dom/smil/crashtests/572938-1.svg new file mode 100644 index 000000000..d759944c7 --- /dev/null +++ b/dom/smil/crashtests/572938-1.svg @@ -0,0 +1,12 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <text id="myText">Used Text Element + <set attributeName="display" to="none"/> + </text> + </defs> + <use xlink:href="#myText" x="20" y="40"/> + <text x="20" y="60">Normal Text Element + <set attributeName="display" to="none"/> + </text> +</svg> diff --git a/dom/smil/crashtests/572938-2.svg b/dom/smil/crashtests/572938-2.svg new file mode 100644 index 000000000..8b9cf7b70 --- /dev/null +++ b/dom/smil/crashtests/572938-2.svg @@ -0,0 +1,22 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + class="reftest-wait"> + + <script> + function boom() + { + document.getElementById("circleID").removeChild( + document.getElementById("at")); + document.documentElement.removeAttribute("class"); + } + window.addEventListener("load", boom, false); + </script> + + <circle id="circleID"> + <animate/> + <animateTransform id="at" attributeName="transform"/> + </circle> + <animate attributeName="stroke-width"/> + <use xlink:href="#circleID"/> + +</svg> diff --git a/dom/smil/crashtests/572938-3.svg b/dom/smil/crashtests/572938-3.svg new file mode 100644 index 000000000..642ad32fb --- /dev/null +++ b/dom/smil/crashtests/572938-3.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <text id="a">Text A</text> + <text id="b">Text B</text> + </defs> + <use xlink:href="#a" x="20" y="40"> + <set attributeName="xlink:href" to="#b" dur="2s"/> + </use> +</svg> diff --git a/dom/smil/crashtests/572938-4.svg b/dom/smil/crashtests/572938-4.svg new file mode 100644 index 000000000..549d43dd6 --- /dev/null +++ b/dom/smil/crashtests/572938-4.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="a"> + <path d=""><animate/></path> +</g> +<g display="none"> +<use xlink:href="#a" x="80"/> +<set attributeName="display" to="inline"/> +</g> +</svg> diff --git a/dom/smil/crashtests/588287-1.svg b/dom/smil/crashtests/588287-1.svg new file mode 100644 index 000000000..cc35cf6b4 --- /dev/null +++ b/dom/smil/crashtests/588287-1.svg @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> + +function boom() +{ + var animate = document.createElementNS("http://www.w3.org/2000/svg", "animate"); + animate.setAttributeNS(null, "begin", "0.5s"); + document.documentElement.appendChild(animate); + + setTimeout(function() { + var g = document.createElement("g"); + var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + g.appendChild(svg); + document.documentElement.appendChild(g); + svg.appendChild(animate); + document.documentElement.removeAttribute("class"); + }, 400); +} + +window.addEventListener("load", function() { setTimeout(boom, 200) }, false); + +</script> +</svg> diff --git a/dom/smil/crashtests/588287-2.svg b/dom/smil/crashtests/588287-2.svg new file mode 100644 index 000000000..70d8e7639 --- /dev/null +++ b/dom/smil/crashtests/588287-2.svg @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> + +function boom() +{ + var animate = document.createElementNS("http://www.w3.org/2000/svg", "animate"); + animate.setAttributeNS(null, "begin", "0.5s"); + document.documentElement.appendChild(animate); + + setTimeout(function() { + var g = document.createElement("g"); + var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + g.appendChild(svg); + document.documentElement.appendChild(g); + svg.setCurrentTime(0.2); + svg.appendChild(animate); + svg.setCurrentTime(0.0); // Trigger backwards seek + document.documentElement.removeAttribute("class"); + }, 400); +} + +window.addEventListener("load", function() { setTimeout(boom, 200) }, false); + +</script> +</svg> diff --git a/dom/smil/crashtests/590425-1.html b/dom/smil/crashtests/590425-1.html new file mode 100644 index 000000000..3cd4805d9 --- /dev/null +++ b/dom/smil/crashtests/590425-1.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> + +function boom() +{ + var frame = document.getElementById("frame") + var frameSVG = frame.contentDocument.getElementById('s'); + var animate = frame.contentDocument.createElementNS("http://www.w3.org/2000/svg", "animate"); + frame.parentNode.removeChild(frame); + frameSVG.appendChild(animate); + document.documentElement.removeAttribute("class"); +} + +</script> +</head> + +<body onload="boom()"> + +<iframe id="frame" src="data:text/html,%3Cbody%3E%3Csvg id=s%3E"></iframe> + +</body> +</html> diff --git a/dom/smil/crashtests/592477-1.xhtml b/dom/smil/crashtests/592477-1.xhtml new file mode 100644 index 000000000..c44a470a3 --- /dev/null +++ b/dom/smil/crashtests/592477-1.xhtml @@ -0,0 +1,26 @@ +<xht:table xmlns:xht="http://www.w3.org/1999/xhtml" + xmlns:xbl="http://www.mozilla.org/xbl"> + <xht:script type="text/javascript">// <![CDATA[ + function boom() { + var topRight = document.getElementById('topRight'); + topRight.appendChild(document.createElement("g")); + } +//]]> </xht:script> + +<xbl:bindings> + <xbl:binding> + <svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + onload="setTimeout(boom, 0)"> + <defs> + <g id="markerGroup"> + <g id="topLeft"/> + <g id="topRight"/> + <set xlink:href="#topLeft" attributeName="fill" to="green" dur="1s"/> + </g> + </defs> + <use xlink:href="#markerGroup"/> + </svg> + </xbl:binding> +</xbl:bindings> +</xht:table> diff --git a/dom/smil/crashtests/594653-1.svg b/dom/smil/crashtests/594653-1.svg new file mode 100644 index 000000000..76352ce30 --- /dev/null +++ b/dom/smil/crashtests/594653-1.svg @@ -0,0 +1,26 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +<![CDATA[ + +window.addEventListener("load", boom, false); + +function boom() +{ + setTimeout(bang, 0); +} + +function bang() +{ + document.documentElement.setCurrentTime(0); + document.documentElement.removeAttribute("class"); +} + +]]> +</script> + +<animate id="b"/> +<animate end="b.end"/> + +</svg> diff --git a/dom/smil/crashtests/596796-1.svg b/dom/smil/crashtests/596796-1.svg new file mode 100644 index 000000000..52a66fd58 --- /dev/null +++ b/dom/smil/crashtests/596796-1.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> + +<script> +function boom() +{ + document.documentElement.appendChild(document.getElementById("a")); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); +</script> + +<animate end="a.begin" id="a"/> + +</svg> diff --git a/dom/smil/crashtests/605345-1.svg b/dom/smil/crashtests/605345-1.svg new file mode 100644 index 000000000..94887cf71 --- /dev/null +++ b/dom/smil/crashtests/605345-1.svg @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +<![CDATA[ + +function boom() +{ + var anim = document.getElementById("a"); + var newSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + var oldSvg = document.removeChild(document.documentElement); + document.appendChild(newSvg); + document.removeChild(document.documentElement); + newSvg.pauseAnimations(); + document.appendChild(newSvg); + newSvg.appendChild(anim); + + oldSvg.removeAttribute("class"); +} + +window.addEventListener("load", function() { setTimeout(boom, 200); }, false); + +]]> +</script> +<animate id="a"/> +</svg> diff --git a/dom/smil/crashtests/606101-1.svg b/dom/smil/crashtests/606101-1.svg new file mode 100644 index 000000000..988c86fa3 --- /dev/null +++ b/dom/smil/crashtests/606101-1.svg @@ -0,0 +1,23 @@ +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> +<script> + +function boom() +{ + var origSVG = document.documentElement; + + var a = document.createElementNS("http://www.w3.org/2000/svg", "animate"); + var g = document.createElementNS("http://www.w3.org/2000/svg", "g"); + var s = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + document.removeChild(document.documentElement); + document.appendChild(g); + s.appendChild(a); + g.appendChild(s); + + origSVG.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +</script> +</svg> diff --git a/dom/smil/crashtests/608295-1.html b/dom/smil/crashtests/608295-1.html new file mode 100644 index 000000000..354e6f909 --- /dev/null +++ b/dom/smil/crashtests/608295-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> + +function boom() +{ + // NB: <script src> is needed to trigger the bug. I'm being clever by also using it to remove reftest-wait. + var s = "<script src='data:text/javascript,parent.document.documentElement.className=null;'><\/script><svg>"; + document.getElementById("f").contentDocument.write(s); +} + +</script> +</head> +<body onload="boom();"> +<iframe id="f"></iframe> +</body> +</html> diff --git a/dom/smil/crashtests/608549-1.svg b/dom/smil/crashtests/608549-1.svg new file mode 100644 index 000000000..dd441e013 --- /dev/null +++ b/dom/smil/crashtests/608549-1.svg @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +<![CDATA[ + +function boom() +{ + try { + document.getElementById("set").beginElementAt(NaN); + return; + } catch (e) {} + try { + document.getElementById("set").endElementAt(NaN); + return; + } catch (e) {} + + // If we got here we threw both exceptions and skipped both early-returns, as + // expected. + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +<set id="set" attributeName="fill" to="green" begin="indefinite"/> + +</svg> diff --git a/dom/smil/crashtests/611927-1.svg b/dom/smil/crashtests/611927-1.svg new file mode 100644 index 000000000..ea60f4ce1 --- /dev/null +++ b/dom/smil/crashtests/611927-1.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <animate attributeName="stroke-width"/> + <animate attributeName="stroke-width" by="10em"/> +</svg> diff --git a/dom/smil/crashtests/615002-1.svg b/dom/smil/crashtests/615002-1.svg new file mode 100644 index 000000000..eb9a29319 --- /dev/null +++ b/dom/smil/crashtests/615002-1.svg @@ -0,0 +1,16 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +function boom() +{ + var a = document.getElementById("a"); + a.removeAttribute('dur'); + document.documentElement.appendChild(a); + // Force a sample + document.documentElement.setCurrentTime(0); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); +</script> +<animate begin="-2s" dur="2s" id="a"/> +</svg> diff --git a/dom/smil/crashtests/615872-1.svg b/dom/smil/crashtests/615872-1.svg new file mode 100644 index 000000000..e0cdf2154 --- /dev/null +++ b/dom/smil/crashtests/615872-1.svg @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"><script> +<![CDATA[ + +function boom() +{ + var r = document.documentElement; + var s = document.createElementNS("http://www.w3.org/2000/svg", "set"); + s.setAttributeNS(null, "begin", "1s"); + r.appendChild(s); + r.setCurrentTime(2); + document.removeChild(r); + r.setCurrentTime(0); + s.beginElementAt(0); +} + +window.addEventListener("load", boom, false); + +]]> +</script></svg> diff --git a/dom/smil/crashtests/641388-1.html b/dom/smil/crashtests/641388-1.html new file mode 100644 index 000000000..4d1f0ea04 --- /dev/null +++ b/dom/smil/crashtests/641388-1.html @@ -0,0 +1,98 @@ +<script>
+
+var ar = new Array(100000);
+
+function fill() {
+ var s = unescape("%ubeef%udead%udead%udead%u0000%u0000%u3030%u3030");
+ while(s.length < 0x40000) {
+ for(var x=0; x<100; x++) ar.push(s+s);
+ s+=s;
+ }
+}
+
+
+function gc() {
+ var evt = document.createEvent("Events");
+ evt.initEvent("please-gc", true, false);
+ document.dispatchEvent(evt);
+ fill();
+}
+
+
+gc();
+function start(){
+tmp = document.createElement('iframe');
+tmp.src="data:image/svg+xml,"+escape("<?xml version='1.0' standalone='no'?><!DOCTYPE svg><svg xmlns='http://www.w3.org/2000/svg'><defs id='element1'></defs><g id='element5'></g></svg>");
+tmp.id = 'ifr23282';
+try{document.getElementById('store_div').appendChild(tmp);}catch(e){}
+window.setTimeout('startrly()', 100);
+} function startrly() {
+try{o6=document.createComment(null);}catch(e){}
+try{o9=document.getElementById('ifr23282').contentDocument.documentElement;;}catch(e){}
+try{o13=document.getElementById('ifr23282').contentDocument.getElementById('element1');;}catch(e){}
+try{o15=document.getElementById('ifr23282').contentDocument.getElementById('element5');;}catch(e){}
+try{tmp = document.createElement('iframe');}catch(e){}
+try{tmp.id = 'ifr6690';}catch(e){}
+try{o6.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe0()',100);
+} function start_dataiframe0(){
+try{o19=o6.ownerDocument.getElementById('ifr6690').contentDocument.documentElement;;}catch(e){}
+try{o24=document.createElementNS('http://www.w3.org/1998/Math/MathML','annotation-xml');;}catch(e){}
+try{o35=document.createElementNS('http://www.w3.org/1998/Math/MathML','emptyset');;}catch(e){}
+try{o40=o19.cloneNode(false);;}catch(e){}
+try{o19.appendChild(o13);}catch(e){}
+try{o19.appendChild(o15);}catch(e){}
+try{o24.appendChild(o40);}catch(e){}
+try{tmp = document.createElement('iframe');}catch(e){}
+tmp.src="data:text/html,<article%20id='element1'></article><command%20id='element3'></command>";
+try{tmp.id = 'ifr17516';}catch(e){}
+try{o13.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe4()',100);
+} function start_dataiframe4(){
+try{o62=o13.ownerDocument.getElementById('ifr17516').contentDocument.getElementById('element1');;}catch(e){}
+try{tmp.id = 'ifr2522';}catch(e){}
+try{o101=o15.ownerDocument.getElementById('ifr2522').contentDocument.getElementById('element3');;}catch(e){}
+try{o101.appendChild(o24);}catch(e){}
+try{o109=o35.setUserData('key',null,function (o,k,d,s,ds) { gc(); });;}catch(e){}
+try{o112=document.createElementNS('http://www.w3.org/1999/xhtml', 'script');;}catch(e){}
+try{o124=document.createElementNS('http://www.w3.org/1998/Math/MathML','root');;}catch(e){}
+try{o125=document.createElementNS('http://www.w3.org/2000/svg','font-face');;}catch(e){}
+gc()
+try{o150=o40;}catch(e){}
+try{tmp.id = 'ifr44501';}catch(e){}
+try{o124.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe7()',100);
+} function start_dataiframe7(){
+try{o152=o124.ownerDocument.getElementById('ifr44501').contentDocument.documentElement;;}catch(e){}
+try{tmp = document.createElement('iframe');}catch(e){}
+try{tmp.src="data:text/html,<div%20id='element1'></div>";}catch(e){}
+try{tmp.id = 'ifr55543';}catch(e){}
+try{o125.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe10()',100);
+} function start_dataiframe10(){
+try{o198=o125.ownerDocument.getElementById('ifr55543').contentDocument.getElementById('element1');;}catch(e){}
+try{o152.appendChild(o101);}catch(e){}
+try{o152.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe17()',100);
+} function start_dataiframe17(){
+try{o286=o152.ownerDocument.getElementById('ifr55543').contentDocument.documentElement;;}catch(e){}
+try{o288=o152.ownerDocument.getElementById('ifr55543').contentDocument.getElementById('element1');;}catch(e){}
+try{o349=document.createElementNS('http://www.w3.org/2000/svg','animate');;}catch(e){}
+try{o150.appendChild(o349);}catch(e){}
+try{o288.appendChild(o150);}catch(e){}
+try{o198.appendChild(o349);}catch(e){}
+window.setTimeout('start_dataiframe24()',100);
+} function start_dataiframe24(){
+try{o286.appendChild(o9);}catch(e){}
+try{o62.appendChild(o152);}catch(e){}
+try{o112.appendChild(o286);}catch(e){}
+try{o534=o35.cloneNode(false);;}catch(e){}
+gc();
+o35 = null;
+gc();
+window.setTimeout("fill()",300);
+}
+</script>
+<body onload="start()">
+<div id="store_div"></div>
+</body>
diff --git a/dom/smil/crashtests/641388-2.html b/dom/smil/crashtests/641388-2.html new file mode 100644 index 000000000..9790e0f54 --- /dev/null +++ b/dom/smil/crashtests/641388-2.html @@ -0,0 +1,79 @@ +<script>
+function gc() {
+ var evt = document.createEvent("Events");
+ evt.initEvent("please-gc", true, false);
+ document.dispatchEvent(evt);
+ }
+var ar =new Array(100000);
+function fill() {
+ var s = unescape("%u0000%u0000%u3030%u3030");
+ while(s.length < 0x40000) {
+ for(var x=0; x<100; x++) ar.push(s+s);
+ s+=s;
+ }
+}
+
+
+function start(){
+tmp = document.createElement('iframe'); 'ifr16727';
+document.documentElement.appendChild(tmp);
+window.setTimeout('start_dataiframe0()',100);
+} function start_dataiframe0(){
+o20=document.createElement('iframe');
+tmp.id = 'ifr4446';;
+o68=o20;
+o101=document.getElementById('ifr4446').contentDocument.createElement('thead');;
+tmp.src="data:text/html," + escape("<html id='element0'><noscript id='element1'></html>");
+tmp.id = 'ifr49879';
+window.setTimeout('start_dataiframe6()',100);
+} function start_dataiframe6(){
+o104=document.getElementById('ifr49879').contentDocument.getElementById('element0');;
+o105=document.getElementById('ifr49879').contentDocument.getElementById('element1');;
+o120=document.getElementById('ifr49879').contentDocument.createElement('figure');;
+o105.appendChild(o120);
+o122=o105.lastElementChild;
+o140=document.getElementById('ifr49879').contentDocument.createElement('style');;
+o141=document.getElementById('ifr49879').contentDocument.createElementNS('http://www.w3.org/2000/svg','animate');;
+o151=o141.cloneNode(true);;
+tmp = document.createElement('iframe');
+tmp.src='data:text/html,%3Cform%20style%3B%27%20id%3D%27element3%27%3E%20%3Caside%20style%20id%3D%27element4%27%%3E';
+tmp.id = 'ifr13645';
+document.documentElement.appendChild(tmp);
+window.setTimeout('start_dataiframe8()',100);
+} function start_dataiframe8(){
+o154=document.getElementById('ifr13645').contentDocument.documentElement;;
+o158=document.getElementById('ifr13645').contentDocument.getElementById('element3');;
+o159=document.getElementById('ifr13645').contentDocument.getElementById('element4');;
+tmp.id = 'ifr17164';
+o120.ownerDocument.documentElement.appendChild(tmp);
+o171=o120.ownerDocument.getElementById('ifr17164').contentDocument.documentElement;;
+tmp = o158.ownerDocument.createElement('iframe');
+o101.appendChild(o151);
+o122.appendChild(o154);
+o68.appendChild(o171);
+o179=document.createElement('tbody');;
+o154.addEventListener('DOMNodeRemoved',function (event) { gc(); },false);
+tmp.src='data:text/html,%3Cs%27%20id%3D%27element0%27element4%27%3E%3Cs%20id%3D%27element5%27%20style%3D%27text-indent%3A%20-1%25%3Bmin-w%2C%20rgba%28255%2C0%2C0%2C0%29%20strict%3Bcolumn-count7element9%27%3E%s%3E';
+tmp.id = 'ifr35960';
+o154.ownerDocument.documentElement.appendChild(tmp);
+window.setTimeout('start_dataiframe13()',100);
+} function start_dataiframe13(){
+o217=o154.ownerDocument.getElementById('ifr35960').contentDocument.documentElement;;
+o218=o154.ownerDocument.getElementById('ifr35960').contentDocument.getElementById('element0');;
+o223=o154.ownerDocument.getElementById('ifr35960').contentDocument.getElementById('element5');;
+o223.appendChild(o101);
+o218.appendChild(o140);
+o140.appendChild(o151);
+o104.appendChild(o179);
+o230=o120.ownerDocument.getElementById('ifr17164').contentDocument.createElementNS('http://www.w3.org/2000/svg','svg');;
+window.setTimeout('start_dataiframe14()',100);
+} function start_dataiframe14(){
+gc();fill();
+o140.appendChild(o230);
+o171.appendChild(o104);
+o159.appendChild(o217);
+o158.appendChild(o218);
+}
+window.setTimeout("start()",100);
+</script>
+
diff --git a/dom/smil/crashtests/650732-1.svg b/dom/smil/crashtests/650732-1.svg new file mode 100644 index 000000000..95be31c16 --- /dev/null +++ b/dom/smil/crashtests/650732-1.svg @@ -0,0 +1,46 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> + <rect fill="green" width="100" height="100"> + <set id="a" attributeName="fill" to="blue" + begin="6s" end="986s"/> + <set id="b" attributeName="fill" to="orange" + begin="a.begin+69.3s;b.begin+700s" dur="700s" end="a.end"/> + <set id="c" attributeName="fill" to="yellow" + begin="0s;b.begin+700s"/> + </rect> + <script type="text/javascript"> +<![CDATA[ +const max_attempts = 100; +var attempts = 0; +function attemptCrash() +{ + remove(); + add(); + if (++attempts >= max_attempts) { + document.documentElement.removeAttribute("class"); + } else { + setTimeout(attemptCrash, 0); + } +} +function add() +{ + const svgns = "http://www.w3.org/2000/svg"; + var elem = document.createElementNS(svgns, "set"); + elem.setAttribute("id", "b"); + elem.setAttribute("attributeName", "fill"); + elem.setAttribute("to", "orange"); + elem.setAttribute("begin", "a.begin+69.3s;b.begin+700s"); + elem.setAttribute("dur", "700s"); + elem.setAttribute("end", "a.end"); + rect = document.getElementsByTagNameNS(svgns, "rect")[0]; + rect.appendChild(elem); +} +function remove() +{ + var elem = document.getElementById('b'); + elem.parentNode.removeChild(elem); + elem = null; +} +window.addEventListener("load", attemptCrash, false); +]]> + </script> +</svg> diff --git a/dom/smil/crashtests/665334-1.svg b/dom/smil/crashtests/665334-1.svg new file mode 100644 index 000000000..94916d1e0 --- /dev/null +++ b/dom/smil/crashtests/665334-1.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +function boom() +{ + // Remove the first 'a'. + document.documentElement.removeChild(document.getElementById("a")); + document.documentElement.removeAttribute("class"); +} +window.addEventListener("load", boom, false); +</script> +<animate id="a"/> +<animate id="a" end="a.begin" /> +</svg> diff --git a/dom/smil/crashtests/669225-1.svg b/dom/smil/crashtests/669225-1.svg new file mode 100644 index 000000000..966010563 --- /dev/null +++ b/dom/smil/crashtests/669225-1.svg @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"> + +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + document.documentElement.appendChild(document.getElementById("a")); +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +<animate end="a.begin" id="a"/> +<animate end="a.begin" id="a"/> + +</svg> diff --git a/dom/smil/crashtests/669225-2.svg b/dom/smil/crashtests/669225-2.svg new file mode 100644 index 000000000..00d52c1f4 --- /dev/null +++ b/dom/smil/crashtests/669225-2.svg @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"> + +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + var a = document.getElementById("a"); + a.removeAttribute("end"); + a.setAttribute("end", "a.begin"); +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +<animate end="0" id="a" onend="boom()"/> +</svg> diff --git a/dom/smil/crashtests/670313-1.svg b/dom/smil/crashtests/670313-1.svg new file mode 100644 index 000000000..97e12f35a --- /dev/null +++ b/dom/smil/crashtests/670313-1.svg @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + try { + document.getElementById("x").beginElementAt(36028797018963970); + } catch (e) { } + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +]]> +</script> +<animate id="x" begin="a" /> +</svg> diff --git a/dom/smil/crashtests/678822-1.svg b/dom/smil/crashtests/678822-1.svg new file mode 100644 index 000000000..a5e81ee10 --- /dev/null +++ b/dom/smil/crashtests/678822-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <animate repeatCount="2" dur="1s" accumulate="1" /> +</svg> diff --git a/dom/smil/crashtests/678847-1.svg b/dom/smil/crashtests/678847-1.svg new file mode 100644 index 000000000..1fa2718cb --- /dev/null +++ b/dom/smil/crashtests/678847-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> +<animate id="a" end="a.end+6s" /> +</svg> diff --git a/dom/smil/crashtests/678938-1.svg b/dom/smil/crashtests/678938-1.svg new file mode 100644 index 000000000..f3f8308fa --- /dev/null +++ b/dom/smil/crashtests/678938-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> + window.addEventListener("load", function() { + setTimeout(function() { + document.documentElement.setCurrentTime(0); + document.documentElement.removeAttribute("class"); + }, 0); + }, false); +</script> +<set id="c"/><set id="b" begin="c.begin; b.begin"/> +</svg> diff --git a/dom/smil/crashtests/690994-1.svg b/dom/smil/crashtests/690994-1.svg new file mode 100644 index 000000000..252fd2c26 --- /dev/null +++ b/dom/smil/crashtests/690994-1.svg @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait" onload="go()"> +<script> +<![CDATA[ +function go() { + document.documentElement.setCurrentTime(100); +} +function boom() +{ + document.documentElement.removeChild(document.getElementById("a")); + document.documentElement.removeAttribute("class"); +} +]]> +</script> +<animate id="a" begin="a.end; 99.9s" end="a.begin+0.2s" onend="boom()"/> +<animate id="a" begin="a.end; 99.9s" end="a.begin+0.2s"/> +</svg> diff --git a/dom/smil/crashtests/691337-1.svg b/dom/smil/crashtests/691337-1.svg new file mode 100644 index 000000000..c341faa6b --- /dev/null +++ b/dom/smil/crashtests/691337-1.svg @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> + <rect width="100" height="100" fill="blue"> + <animate attributeName="fill" + begin="999999999999999999999999999999999999999999999999999999999999999999999999999999999" + dur="5s" from="blue" to="red" repeatCount="indefinite" additive="sum"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/691337-2.svg b/dom/smil/crashtests/691337-2.svg new file mode 100644 index 000000000..f4408ae5e --- /dev/null +++ b/dom/smil/crashtests/691337-2.svg @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> + <rect width="100" height="100" fill="blue"> + <animate attributeName="fill" id="a" + begin="4999999999999999" dur="5s" from="blue" to="red" + repeatCount="indefinite" additive="sum"/> + <animate attributeName="fill" + begin="a.begin+4999999999999999" + dur="5s" from="blue" to="red" repeatCount="indefinite" additive="sum"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/697640-1.svg b/dom/smil/crashtests/697640-1.svg new file mode 100644 index 000000000..c2e1b89fd --- /dev/null +++ b/dom/smil/crashtests/697640-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <animate id="b" end="b.end" dur="3s" /> +</svg> diff --git a/dom/smil/crashtests/699325-1.svg b/dom/smil/crashtests/699325-1.svg new file mode 100644 index 000000000..7496c6ae2 --- /dev/null +++ b/dom/smil/crashtests/699325-1.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <rect fill="blue" height="100" width="100"> + <animate id="a" attributeName="x" calcMode="paced" values="50; 50; 50"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/709907-1.svg b/dom/smil/crashtests/709907-1.svg new file mode 100644 index 000000000..631911970 --- /dev/null +++ b/dom/smil/crashtests/709907-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <animate attributeName="stroke-dasharray" from="-3" /> +</svg> diff --git a/dom/smil/crashtests/720103-1.svg b/dom/smil/crashtests/720103-1.svg new file mode 100644 index 000000000..a51a3bf0f --- /dev/null +++ b/dom/smil/crashtests/720103-1.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> +<animate id="a" begin="-3.1s" end="a.begin+0.2s"/> +</svg> diff --git a/dom/smil/crashtests/crashtests.list b/dom/smil/crashtests/crashtests.list new file mode 100644 index 000000000..f783bb49e --- /dev/null +++ b/dom/smil/crashtests/crashtests.list @@ -0,0 +1,54 @@ +load 483584-1.svg +load 483584-2.svg +load 523188-1.svg +load 525099-1.svg +load 526536-1.svg +load 526875-1.svg +load 526875-2.svg +load 529387-1.xhtml +load 531550-1.svg +load 537157-1.svg +load 541297-1.svg +load 547333-1.svg +load 548899-1.svg +load 551620-1.svg +load 554141-1.svg +load 554202-1.svg +load 554202-2.svg +load 555026-1.svg +load 556841-1.svg +load 572938-1.svg +load 572938-2.svg +load 572938-3.svg +load 572938-4.svg +load 588287-1.svg +load 588287-2.svg +load 590425-1.html +load 592477-1.xhtml +load 594653-1.svg +load 596796-1.svg +load 605345-1.svg +load 606101-1.svg +load 608295-1.html +load 608549-1.svg +load 611927-1.svg +load 615002-1.svg +load 615872-1.svg +load 641388-1.html +load 641388-2.html +load 650732-1.svg +load 665334-1.svg +load 669225-1.svg +load 669225-2.svg +load 670313-1.svg +load 678822-1.svg +load 678847-1.svg +load 678938-1.svg +load 690994-1.svg +load 691337-1.svg +load 691337-2.svg +load 697640-1.svg +load 699325-1.svg +load 709907-1.svg +load 720103-1.svg +load 1010681-1.svg diff --git a/dom/smil/moz.build b/dom/smil/moz.build new file mode 100644 index 000000000..5b37ce642 --- /dev/null +++ b/dom/smil/moz.build @@ -0,0 +1,72 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +MOCHITEST_MANIFESTS += ['test/mochitest.ini'] + +EXPORTS += [ + 'nsISMILAttr.h', + 'nsISMILType.h', + 'nsSMILAnimationController.h', + 'nsSMILAnimationFunction.h', + 'nsSMILCompositorTable.h', + 'nsSMILCSSProperty.h', + 'nsSMILInstanceTime.h', + 'nsSMILInterval.h', + 'nsSMILKeySpline.h', + 'nsSMILMappedAttribute.h', + 'nsSMILMilestone.h', + 'nsSMILNullType.h', + 'nsSMILRepeatCount.h', + 'nsSMILSetAnimationFunction.h', + 'nsSMILTargetIdentifier.h', + 'nsSMILTimeContainer.h', + 'nsSMILTimedElement.h', + 'nsSMILTimeValue.h', + 'nsSMILTimeValueSpec.h', + 'nsSMILTimeValueSpecParams.h', + 'nsSMILTypes.h', + 'nsSMILValue.h', +] + +EXPORTS.mozilla.dom += [ + 'TimeEvent.h', +] + +UNIFIED_SOURCES += [ + 'nsSMILAnimationController.cpp', + 'nsSMILAnimationFunction.cpp', + 'nsSMILCompositor.cpp', + 'nsSMILCSSProperty.cpp', + 'nsSMILCSSValueType.cpp', + 'nsSMILFloatType.cpp', + 'nsSMILInstanceTime.cpp', + 'nsSMILInterval.cpp', + 'nsSMILKeySpline.cpp', + 'nsSMILMappedAttribute.cpp', + 'nsSMILNullType.cpp', + 'nsSMILParserUtils.cpp', + 'nsSMILRepeatCount.cpp', + 'nsSMILSetAnimationFunction.cpp', + 'nsSMILTimeContainer.cpp', + 'nsSMILTimedElement.cpp', + 'nsSMILTimeValue.cpp', + 'nsSMILTimeValueSpec.cpp', + 'nsSMILValue.cpp', + 'SMILBoolType.cpp', + 'SMILEnumType.cpp', + 'SMILIntegerType.cpp', + 'SMILStringType.cpp', + 'TimeEvent.cpp', +] + +LOCAL_INCLUDES += [ + '/dom/base', + '/dom/svg', + '/layout/base', + '/layout/style', +] + +FINAL_LIBRARY = 'xul' diff --git a/dom/smil/nsISMILAttr.h b/dom/smil/nsISMILAttr.h new file mode 100644 index 000000000..de65a42bb --- /dev/null +++ b/dom/smil/nsISMILAttr.h @@ -0,0 +1,98 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_ISMILATTR_H_ +#define NS_ISMILATTR_H_ + +#include "nscore.h" + +class nsSMILValue; +class nsIContent; +class nsAString; + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +//////////////////////////////////////////////////////////////////////// +// nsISMILAttr: A variable targeted by SMIL for animation and can therefore have +// an underlying (base) value and an animated value For example, an attribute of +// a particular SVG element. +// +// These objects only exist during the compositing phase of SMIL animation +// calculations. They have a single owner who is responsible for deleting the +// object. + +class nsISMILAttr +{ +public: + /** + * Creates a new nsSMILValue for this attribute from a string. The string is + * parsed in the context of this attribute so that context-dependent values + * such as em-based units can be resolved into a canonical form suitable for + * animation (including interpolation etc.). + * + * @param aStr A string defining the new value to be created. + * @param aSrcElement The source animation element. This may be needed to + * provided additional context data such as for + * animateTransform where the 'type' attribute is needed to + * parse the value. + * @param[out] aValue Outparam for storing the parsed value. + * @param[out] aPreventCachingOfSandwich + * Outparam to indicate whether the attribute contains + * dependencies on its context that should prevent the + * result of the animation sandwich from being cached and + * reused in future samples. + * @return NS_OK on success or an error code if creation failed. + */ + virtual nsresult ValueFromString(const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const = 0; + + /** + * Gets the underlying value of this attribute. + * + * @return an nsSMILValue object. returned_object.IsNull() will be true if an + * error occurred. + */ + virtual nsSMILValue GetBaseValue() const = 0; + + /** + * Clears the animated value of this attribute. + * + * NOTE: The animation target is not guaranteed to be in a document when this + * method is called. (See bug 523188) + */ + virtual void ClearAnimValue() = 0; + + /** + * Sets the presentation value of this attribute. + * + * @param aValue The value to set. + * @return NS_OK on success or an error code if setting failed. + */ + virtual nsresult SetAnimValue(const nsSMILValue& aValue) = 0; + + /** + * Returns the targeted content node, for any nsISMILAttr implementations + * that want to expose that to the animation logic. Otherwise, returns + * null. + * + * @return the targeted content node, if this nsISMILAttr implementation + * wishes to make it avaiable. Otherwise, nullptr. + */ + virtual const nsIContent* GetTargetNode() const { return nullptr; } + + /** + * Virtual destructor, to make sure subclasses can clean themselves up. + */ + virtual ~nsISMILAttr() {} +}; + +#endif // NS_ISMILATTR_H_ diff --git a/dom/smil/nsISMILType.h b/dom/smil/nsISMILType.h new file mode 100644 index 000000000..f923a9c77 --- /dev/null +++ b/dom/smil/nsISMILType.h @@ -0,0 +1,214 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_ISMILTYPE_H_ +#define NS_ISMILTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nscore.h" + +class nsSMILValue; + +////////////////////////////////////////////////////////////////////////////// +// nsISMILType: Interface for defining the basic operations needed for animating +// a particular kind of data (e.g. lengths, colors, transformation matrices). +// +// This interface is never used directly but always through an nsSMILValue that +// bundles together a pointer to a concrete implementation of this interface and +// the data upon which it should operate. +// +// We keep the data and type separate rather than just providing different +// subclasses of nsSMILValue. This is so that sizeof(nsSMILValue) is the same +// for all value types, allowing us to have a type-agnostic nsTArray of +// nsSMILValue objects (actual objects, not pointers). It also allows most +// nsSMILValues (except those that need to allocate extra memory for their +// data) to be allocated on the stack and directly assigned to one another +// provided performance benefits for the animation code. +// +// Note that different types have different capabilities. Roughly speaking there +// are probably three main types: +// +// +---------------------+---------------+-------------+------------------+ +// | CATEGORY: | DISCRETE | LINEAR | ADDITIVE | +// +---------------------+---------------+-------------+------------------+ +// | Example: | strings, | path data? | lengths, | +// | | color k/words?| | RGB color values | +// | | | | | +// | -- Assign? | X | X | X | +// | -- Add? | - | X? | X | +// | -- SandwichAdd? | - | -? | X | +// | -- ComputeDistance? | - | - | X? | +// | -- Interpolate? | - | X | X | +// +---------------------+---------------+-------------+------------------+ +// + +class nsISMILType +{ + /** + * Only give the nsSMILValue class access to this interface. + */ + friend class nsSMILValue; + +protected: + /** + * Initialises aValue and sets it to some identity value such that adding + * aValue to another value of the same type has no effect. + * + * @pre aValue.IsNull() + * @post aValue.mType == this + */ + virtual void Init(nsSMILValue& aValue) const = 0; + + /** + * Destroys any data associated with a value of this type. + * + * @pre aValue.mType == this + * @post aValue.IsNull() + */ + virtual void Destroy(nsSMILValue& aValue) const = 0; + + /** + * Assign this object the value of another. Think of this as the assignment + * operator. + * + * @param aDest The left-hand side of the assignment. + * @param aSrc The right-hand side of the assignment. + * @return NS_OK on success, an error code on failure such as when the + * underlying type of the specified object differs. + * + * @pre aDest.mType == aSrc.mType == this + */ + virtual nsresult Assign(nsSMILValue& aDest, + const nsSMILValue& aSrc) const = 0; + + /** + * Test two nsSMILValue objects (of this nsISMILType) for equality. + * + * A return value of true represents a guarantee that aLeft and aRight are + * equal. (That is, they would behave identically if passed to the methods + * Add, SandwichAdd, ComputeDistance, and Interpolate). + * + * A return value of false simply indicates that we make no guarantee + * about equality. + * + * NOTE: It's perfectly legal for implementations of this method to return + * false in all cases. However, smarter implementations will make this + * method more useful for optimization. + * + * @param aLeft The left-hand side of the equality check. + * @param aRight The right-hand side of the equality check. + * @return true if we're sure the values are equal, false otherwise. + * + * @pre aDest.mType == aSrc.mType == this + */ + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const = 0; + + /** + * Adds two values. + * + * The count parameter facilitates repetition. + * + * By equation, + * + * aDest += aValueToAdd * aCount + * + * Therefore, if aCount == 0, aDest will be unaltered. + * + * This method will fail if this data type is not additive or the value was + * not specified using an additive syntax. + * + * See SVG 1.1, section 19.2.5. In particular, + * + * "If a given attribute or property can take values of keywords (which are + * not additive) or numeric values (which are additive), then additive + * animations are possible if the subsequent animation uses a numeric value + * even if the base animation uses a keyword value; however, if the + * subsequent animation uses a keyword value, additive animation is not + * possible." + * + * If this method fails (e.g. because the data type is not additive), aDest + * will be unaltered. + * + * @param aDest The value to add to. + * @param aValueToAdd The value to add. + * @param aCount The number of times to add aValueToAdd. + * @return NS_OK on success, an error code on failure. + * + * @pre aValueToAdd.mType == aDest.mType == this + */ + virtual nsresult Add(nsSMILValue& aDest, + const nsSMILValue& aValueToAdd, + uint32_t aCount) const = 0; + + /** + * Adds aValueToAdd to the underlying value in the animation sandwich, aDest. + * + * For most types this operation is identical to a regular Add() but for some + * types (notably <animateTransform>) the operation differs. For + * <animateTransform> Add() corresponds to simply adding together the + * transform parameters and is used when calculating cumulative values or + * by-animation values. On the other hand SandwichAdd() is used when adding to + * the underlying value and requires matrix post-multiplication. (This + * distinction is most clearly indicated by the SVGT1.2 test suite. It is not + * obvious within the SMIL specifications.) + * + * @param aDest The value to add to. + * @param aValueToAdd The value to add. + * @return NS_OK on success, an error code on failure. + * + * @pre aValueToAdd.mType == aDest.mType == this + */ + virtual nsresult SandwichAdd(nsSMILValue& aDest, + const nsSMILValue& aValueToAdd) const + { + return Add(aDest, aValueToAdd, 1); + } + + /** + * Calculates the 'distance' between two values. This is the distance used in + * paced interpolation. + * + * @param aFrom The start of the interval for which the distance should + * be calculated. + * @param aTo The end of the interval for which the distance should be + * calculated. + * @param aDistance The result of the calculation. + * @return NS_OK on success, or an appropriate error code if there is no + * notion of distance for the underlying data type or the distance + * could not be calculated. + * + * @pre aFrom.mType == aTo.mType == this + */ + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const = 0; + + /** + * Calculates an interpolated value between two values using the specified + * proportion. + * + * @param aStartVal The value defining the start of the interval of + * interpolation. + * @param aEndVal The value defining the end of the interval of + * interpolation. + * @param aUnitDistance A number between 0.0 and 1.0 (inclusive) defining + * the distance of the interpolated value in the + * interval. + * @param aResult The interpolated value. + * @return NS_OK on success, NS_ERROR_FAILURE if this data type cannot be + * interpolated or NS_ERROR_OUT_OF_MEMORY if insufficient memory was + * available for storing the result. + * + * @pre aStartVal.mType == aEndVal.mType == aResult.mType == this + */ + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const = 0; +}; + +#endif // NS_ISMILTYPE_H_ diff --git a/dom/smil/nsSMILAnimationController.cpp b/dom/smil/nsSMILAnimationController.cpp new file mode 100644 index 000000000..0dd616346 --- /dev/null +++ b/dom/smil/nsSMILAnimationController.cpp @@ -0,0 +1,795 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILAnimationController.h" +#include "nsSMILCompositor.h" +#include "nsSMILCSSProperty.h" +#include "nsCSSProps.h" +#include "nsITimer.h" +#include "mozilla/dom/Element.h" +#include "nsIDocument.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsSMILTimedElement.h" +#include <algorithm> +#include "mozilla/AutoRestore.h" +#include "RestyleTracker.h" + +using namespace mozilla; +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// nsSMILAnimationController implementation + +//---------------------------------------------------------------------- +// ctors, dtors, factory methods + +nsSMILAnimationController::nsSMILAnimationController(nsIDocument* aDoc) + : mAvgTimeBetweenSamples(0), + mResampleNeeded(false), + mDeferredStartSampling(false), + mRunningSample(false), + mRegisteredWithRefreshDriver(false), + mMightHavePendingStyleUpdates(false), + mDocument(aDoc) +{ + MOZ_ASSERT(aDoc, "need a non-null document"); + + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + if (refreshDriver) { + mStartTime = refreshDriver->MostRecentRefresh(); + } else { + mStartTime = mozilla::TimeStamp::Now(); + } + mCurrentSampleTime = mStartTime; + + Begin(); +} + +nsSMILAnimationController::~nsSMILAnimationController() +{ + NS_ASSERTION(mAnimationElementTable.Count() == 0, + "Animation controller shouldn't be tracking any animation" + " elements when it dies"); + NS_ASSERTION(!mRegisteredWithRefreshDriver, + "Leaving stale entry in refresh driver's observer list"); +} + +void +nsSMILAnimationController::Disconnect() +{ + MOZ_ASSERT(mDocument, "disconnecting when we weren't connected...?"); + MOZ_ASSERT(mRefCnt.get() == 1, + "Expecting to disconnect when doc is sole remaining owner"); + NS_ASSERTION(mPauseState & nsSMILTimeContainer::PAUSE_PAGEHIDE, + "Expecting to be paused for pagehide before disconnect"); + + StopSampling(GetRefreshDriver()); + + mDocument = nullptr; // (raw pointer) +} + +//---------------------------------------------------------------------- +// nsSMILTimeContainer methods: + +void +nsSMILAnimationController::Pause(uint32_t aType) +{ + nsSMILTimeContainer::Pause(aType); + + if (mPauseState) { + mDeferredStartSampling = false; + StopSampling(GetRefreshDriver()); + } +} + +void +nsSMILAnimationController::Resume(uint32_t aType) +{ + bool wasPaused = (mPauseState != 0); + // Update mCurrentSampleTime so that calls to GetParentTime--used for + // calculating parent offsets--are accurate + mCurrentSampleTime = mozilla::TimeStamp::Now(); + + nsSMILTimeContainer::Resume(aType); + + if (wasPaused && !mPauseState && mChildContainerTable.Count()) { + MaybeStartSampling(GetRefreshDriver()); + Sample(); // Run the first sample manually + } +} + +nsSMILTime +nsSMILAnimationController::GetParentTime() const +{ + return (nsSMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds(); +} + +//---------------------------------------------------------------------- +// nsARefreshObserver methods: +NS_IMPL_ADDREF(nsSMILAnimationController) +NS_IMPL_RELEASE(nsSMILAnimationController) + +// nsRefreshDriver Callback function +void +nsSMILAnimationController::WillRefresh(mozilla::TimeStamp aTime) +{ + // Although we never expect aTime to go backwards, when we initialise the + // animation controller, if we can't get hold of a refresh driver we + // initialise mCurrentSampleTime to Now(). It may be possible that after + // doing so we get sampled by a refresh driver whose most recent refresh time + // predates when we were initialised, so to be safe we make sure to take the + // most recent time here. + aTime = std::max(mCurrentSampleTime, aTime); + + // Sleep detection: If the time between samples is a whole lot greater than we + // were expecting then we assume the computer went to sleep or someone's + // messing with the clock. In that case, fiddle our parent offset and use our + // average time between samples to calculate the new sample time. This + // prevents us from hanging while trying to catch up on all the missed time. + + // Smoothing of coefficient for the average function. 0.2 should let us track + // the sample rate reasonably tightly without being overly affected by + // occasional delays. + static const double SAMPLE_DUR_WEIGHTING = 0.2; + // If the elapsed time exceeds our expectation by this number of times we'll + // initiate special behaviour to basically ignore the intervening time. + static const double SAMPLE_DEV_THRESHOLD = 200.0; + + nsSMILTime elapsedTime = + (nsSMILTime)(aTime - mCurrentSampleTime).ToMilliseconds(); + if (mAvgTimeBetweenSamples == 0) { + // First sample. + mAvgTimeBetweenSamples = elapsedTime; + } else { + if (elapsedTime > SAMPLE_DEV_THRESHOLD * mAvgTimeBetweenSamples) { + // Unexpectedly long delay between samples. + NS_WARNING("Detected really long delay between samples, continuing from " + "previous sample"); + mParentOffset += elapsedTime - mAvgTimeBetweenSamples; + } + // Update the moving average. Due to truncation here the average will + // normally be a little less than it should be but that's probably ok. + mAvgTimeBetweenSamples = + (nsSMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING + + mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING)); + } + mCurrentSampleTime = aTime; + + Sample(); +} + +//---------------------------------------------------------------------- +// Animation element registration methods: + +void +nsSMILAnimationController::RegisterAnimationElement( + SVGAnimationElement* aAnimationElement) +{ + mAnimationElementTable.PutEntry(aAnimationElement); + if (mDeferredStartSampling) { + mDeferredStartSampling = false; + if (mChildContainerTable.Count()) { + // mAnimationElementTable was empty, but now we've added its 1st element + MOZ_ASSERT(mAnimationElementTable.Count() == 1, + "we shouldn't have deferred sampling if we already had " + "animations registered"); + StartSampling(GetRefreshDriver()); + Sample(); // Run the first sample manually + } // else, don't sample until a time container is registered (via AddChild) + } +} + +void +nsSMILAnimationController::UnregisterAnimationElement( + SVGAnimationElement* aAnimationElement) +{ + mAnimationElementTable.RemoveEntry(aAnimationElement); +} + +//---------------------------------------------------------------------- +// Page show/hide + +void +nsSMILAnimationController::OnPageShow() +{ + Resume(nsSMILTimeContainer::PAUSE_PAGEHIDE); +} + +void +nsSMILAnimationController::OnPageHide() +{ + Pause(nsSMILTimeContainer::PAUSE_PAGEHIDE); +} + +//---------------------------------------------------------------------- +// Cycle-collection support + +void +nsSMILAnimationController::Traverse( + nsCycleCollectionTraversalCallback* aCallback) +{ + // Traverse last compositor table + if (mLastCompositorTable) { + for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { + nsSMILCompositor* compositor = iter.Get(); + compositor->Traverse(aCallback); + } + } +} + +void +nsSMILAnimationController::Unlink() +{ + mLastCompositorTable = nullptr; +} + +//---------------------------------------------------------------------- +// Refresh driver lifecycle related methods + +void +nsSMILAnimationController::NotifyRefreshDriverCreated( + nsRefreshDriver* aRefreshDriver) +{ + if (!mPauseState) { + MaybeStartSampling(aRefreshDriver); + } +} + +void +nsSMILAnimationController::NotifyRefreshDriverDestroying( + nsRefreshDriver* aRefreshDriver) +{ + if (!mPauseState && !mDeferredStartSampling) { + StopSampling(aRefreshDriver); + } +} + +//---------------------------------------------------------------------- +// Timer-related implementation helpers + +void +nsSMILAnimationController::StartSampling(nsRefreshDriver* aRefreshDriver) +{ + NS_ASSERTION(mPauseState == 0, "Starting sampling but controller is paused"); + NS_ASSERTION(!mDeferredStartSampling, + "Started sampling but the deferred start flag is still set"); + if (aRefreshDriver) { + MOZ_ASSERT(!mRegisteredWithRefreshDriver, + "Redundantly registering with refresh driver"); + MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(), + "Starting sampling with wrong refresh driver"); + // We're effectively resuming from a pause so update our current sample time + // or else it will confuse our "average time between samples" calculations. + mCurrentSampleTime = mozilla::TimeStamp::Now(); + aRefreshDriver->AddRefreshObserver(this, Flush_Style); + mRegisteredWithRefreshDriver = true; + } +} + +void +nsSMILAnimationController::StopSampling(nsRefreshDriver* aRefreshDriver) +{ + if (aRefreshDriver && mRegisteredWithRefreshDriver) { + // NOTE: The document might already have been detached from its PresContext + // (and RefreshDriver), which would make GetRefreshDriver() return null. + MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(), + "Stopping sampling with wrong refresh driver"); + aRefreshDriver->RemoveRefreshObserver(this, Flush_Style); + mRegisteredWithRefreshDriver = false; + } +} + +void +nsSMILAnimationController::MaybeStartSampling(nsRefreshDriver* aRefreshDriver) +{ + if (mDeferredStartSampling) { + // We've received earlier 'MaybeStartSampling' calls, and we're + // deferring until we get a registered animation. + return; + } + + if (mAnimationElementTable.Count()) { + StartSampling(aRefreshDriver); + } else { + mDeferredStartSampling = true; + } +} + +//---------------------------------------------------------------------- +// Sample-related methods and callbacks + +void +nsSMILAnimationController::DoSample() +{ + DoSample(true); // Skip unchanged time containers +} + +void +nsSMILAnimationController::DoSample(bool aSkipUnchangedContainers) +{ + if (!mDocument) { + NS_ERROR("Shouldn't be sampling after document has disconnected"); + return; + } + if (mRunningSample) { + NS_ERROR("Shouldn't be recursively sampling"); + return; + } + + bool isStyleFlushNeeded = mResampleNeeded; + mResampleNeeded = false; + nsCOMPtr<nsIDocument> document(mDocument); // keeps 'this' alive too + + // Set running sample flag -- do this before flushing styles so that when we + // flush styles we don't end up requesting extra samples + AutoRestore<bool> autoRestoreRunningSample(mRunningSample); + mRunningSample = true; + + // STEP 1: Bring model up to date + // (i) Rewind elements where necessary + // (ii) Run milestone samples + RewindElements(); + DoMilestoneSamples(); + + // STEP 2: Sample the child time containers + // + // When we sample the child time containers they will simply record the sample + // time in document time. + TimeContainerHashtable activeContainers(mChildContainerTable.Count()); + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + nsSMILTimeContainer* container = iter.Get()->GetKey(); + if (!container) { + continue; + } + + if (!container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN) && + (container->NeedsSample() || !aSkipUnchangedContainers)) { + container->ClearMilestones(); + container->Sample(); + container->MarkSeekFinished(); + activeContainers.PutEntry(container); + } + } + + // STEP 3: (i) Sample the timed elements AND + // (ii) Create a table of compositors + // + // (i) Here we sample the timed elements (fetched from the + // SVGAnimationElements) which determine from the active time if the + // element is active and what its simple time etc. is. This information is + // then passed to its time client (nsSMILAnimationFunction). + // + // (ii) During the same loop we also build up a table that contains one + // compositor for each animated attribute and which maps animated elements to + // the corresponding compositor for their target attribute. + // + // Note that this compositor table needs to be allocated on the heap so we can + // store it until the next sample. This lets us find out which elements were + // animated in sample 'n-1' but not in sample 'n' (and hence need to have + // their animation effects removed in sample 'n'). + // + // Parts (i) and (ii) are not functionally related but we combine them here to + // save iterating over the animation elements twice. + + // Create the compositor table + nsAutoPtr<nsSMILCompositorTable> + currentCompositorTable(new nsSMILCompositorTable(0)); + nsTArray<RefPtr<SVGAnimationElement>> + animElems(mAnimationElementTable.Count()); + + for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) { + SVGAnimationElement* animElem = iter.Get()->GetKey(); + SampleTimedElement(animElem, &activeContainers); + AddAnimationToCompositorTable(animElem, + currentCompositorTable, + isStyleFlushNeeded); + animElems.AppendElement(animElem); + } + activeContainers.Clear(); + + // STEP 4: Compare previous sample's compositors against this sample's. + // (Transfer cached base values across, & remove animation effects from + // no-longer-animated targets.) + if (mLastCompositorTable) { + // * Transfer over cached base values, from last sample's compositors + for (auto iter = currentCompositorTable->Iter(); + !iter.Done(); + iter.Next()) { + nsSMILCompositor* compositor = iter.Get(); + nsSMILCompositor* lastCompositor = + mLastCompositorTable->GetEntry(compositor->GetKey()); + + if (lastCompositor) { + compositor->StealCachedBaseValue(lastCompositor); + } + } + + // * For each compositor in current sample's hash table, remove entry from + // prev sample's hash table -- we don't need to clear animation + // effects of those compositors, since they're still being animated. + for (auto iter = currentCompositorTable->Iter(); + !iter.Done(); + iter.Next()) { + mLastCompositorTable->RemoveEntry(iter.Get()->GetKey()); + } + + // * For each entry that remains in prev sample's hash table (i.e. for + // every target that's no longer animated), clear animation effects. + for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { + iter.Get()->ClearAnimationEffects(); + } + } + + // return early if there are no active animations to avoid a style flush + if (currentCompositorTable->Count() == 0) { + mLastCompositorTable = nullptr; + return; + } + + if (isStyleFlushNeeded) { + document->FlushPendingNotifications(Flush_Style); + } + + // WARNING: + // WARNING: the above flush may have destroyed the pres shell and/or + // WARNING: frames and other layout related objects. + // WARNING: + + // STEP 5: Compose currently-animated attributes. + // XXXdholbert: This step traverses our animation targets in an effectively + // random order. For animation from/to 'inherit' values to work correctly + // when the inherited value is *also* being animated, we really should be + // traversing our animated nodes in an ancestors-first order (bug 501183) + bool mightHavePendingStyleUpdates = false; + for (auto iter = currentCompositorTable->Iter(); !iter.Done(); iter.Next()) { + iter.Get()->ComposeAttribute(mightHavePendingStyleUpdates); + } + + // Update last compositor table + mLastCompositorTable = currentCompositorTable.forget(); + mMightHavePendingStyleUpdates = mightHavePendingStyleUpdates; + + NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!"); +} + +void +nsSMILAnimationController::RewindElements() +{ + bool rewindNeeded = false; + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + nsSMILTimeContainer* container = iter.Get()->GetKey(); + if (container->NeedsRewind()) { + rewindNeeded = true; + break; + } + } + + if (!rewindNeeded) + return; + + for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) { + SVGAnimationElement* animElem = iter.Get()->GetKey(); + nsSMILTimeContainer* timeContainer = animElem->GetTimeContainer(); + if (timeContainer && timeContainer->NeedsRewind()) { + animElem->TimedElement().Rewind(); + } + } + + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + iter.Get()->GetKey()->ClearNeedsRewind(); + } +} + +void +nsSMILAnimationController::DoMilestoneSamples() +{ + // We need to sample the timing model but because SMIL operates independently + // of the frame-rate, we can get one sample at t=0s and the next at t=10min. + // + // In between those two sample times a whole string of significant events + // might be expected to take place: events firing, new interdependencies + // between animations resolved and dissolved, etc. + // + // Furthermore, at any given time, we want to sample all the intervals that + // end at that time BEFORE any that begin. This behaviour is implied by SMIL's + // endpoint-exclusive timing model. + // + // So we have the animations (specifically the timed elements) register the + // next significant moment (called a milestone) in their lifetime and then we + // step through the model at each of these moments and sample those animations + // registered for those times. This way events can fire in the correct order, + // dependencies can be resolved etc. + + nsSMILTime sampleTime = INT64_MIN; + + while (true) { + // We want to find any milestones AT OR BEFORE the current sample time so we + // initialise the next milestone to the moment after (1ms after, to be + // precise) the current sample time and see if there are any milestones + // before that. Any other milestones will be dealt with in a subsequent + // sample. + nsSMILMilestone nextMilestone(GetCurrentTime() + 1, true); + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + nsSMILTimeContainer* container = iter.Get()->GetKey(); + if (container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) { + continue; + } + nsSMILMilestone thisMilestone; + bool didGetMilestone = + container->GetNextMilestoneInParentTime(thisMilestone); + if (didGetMilestone && thisMilestone < nextMilestone) { + nextMilestone = thisMilestone; + } + } + + if (nextMilestone.mTime > GetCurrentTime()) { + break; + } + + nsTArray<RefPtr<mozilla::dom::SVGAnimationElement>> elements; + for (auto iter = mChildContainerTable.Iter(); !iter.Done(); iter.Next()) { + nsSMILTimeContainer* container = iter.Get()->GetKey(); + if (container->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) { + continue; + } + container->PopMilestoneElementsAtMilestone(nextMilestone, elements); + } + + uint32_t length = elements.Length(); + + // During the course of a sampling we don't want to actually go backwards. + // Due to negative offsets, early ends and the like, a timed element might + // register a milestone that is actually in the past. That's fine, but it's + // still only going to get *sampled* with whatever time we're up to and no + // earlier. + // + // Because we're only performing this clamping at the last moment, the + // animations will still all get sampled in the correct order and + // dependencies will be appropriately resolved. + sampleTime = std::max(nextMilestone.mTime, sampleTime); + + for (uint32_t i = 0; i < length; ++i) { + SVGAnimationElement* elem = elements[i].get(); + MOZ_ASSERT(elem, "nullptr animation element in list"); + nsSMILTimeContainer* container = elem->GetTimeContainer(); + if (!container) + // The container may be nullptr if the element has been detached from its + // parent since registering a milestone. + continue; + + nsSMILTimeValue containerTimeValue = + container->ParentToContainerTime(sampleTime); + if (!containerTimeValue.IsDefinite()) + continue; + + // Clamp the converted container time to non-negative values. + nsSMILTime containerTime = std::max<nsSMILTime>(0, containerTimeValue.GetMillis()); + + if (nextMilestone.mIsEnd) { + elem->TimedElement().SampleEndAt(containerTime); + } else { + elem->TimedElement().SampleAt(containerTime); + } + } + } +} + +/*static*/ void +nsSMILAnimationController::SampleTimedElement( + SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) +{ + nsSMILTimeContainer* timeContainer = aElement->GetTimeContainer(); + if (!timeContainer) + return; + + // We'd like to call timeContainer->NeedsSample() here and skip all timed + // elements that belong to paused time containers that don't need a sample, + // but that doesn't work because we've already called Sample() on all the time + // containers so the paused ones don't need a sample any more and they'll + // return false. + // + // Instead we build up a hashmap of active time containers during the previous + // step (SampleTimeContainer) and then test here if the container for this + // timed element is in the list. + if (!aActiveContainers->GetEntry(timeContainer)) + return; + + nsSMILTime containerTime = timeContainer->GetCurrentTime(); + + MOZ_ASSERT(!timeContainer->IsSeeking(), + "Doing a regular sample but the time container is still seeking"); + aElement->TimedElement().SampleAt(containerTime); +} + +/*static*/ void +nsSMILAnimationController::AddAnimationToCompositorTable( + SVGAnimationElement* aElement, + nsSMILCompositorTable* aCompositorTable, + bool& aStyleFlushNeeded) +{ + // Add a compositor to the hash table if there's not already one there + nsSMILTargetIdentifier key; + if (!GetTargetIdentifierForAnimation(aElement, key)) + // Something's wrong/missing about animation's target; skip this animation + return; + + nsSMILAnimationFunction& func = aElement->AnimationFunction(); + + // Only add active animation functions. If there are no active animations + // targeting an attribute, no compositor will be created and any previously + // applied animations will be cleared. + if (func.IsActiveOrFrozen()) { + // Look up the compositor for our target, & add our animation function + // to its list of animation functions. + nsSMILCompositor* result = aCompositorTable->PutEntry(key); + result->AddAnimationFunction(&func); + + } else if (func.HasChanged()) { + // Look up the compositor for our target, and force it to skip the + // "nothing's changed so don't bother compositing" optimization for this + // sample. |func| is inactive, but it's probably *newly* inactive (since + // it's got HasChanged() == true), so we need to make sure to recompose + // its target. + nsSMILCompositor* result = aCompositorTable->PutEntry(key); + result->ToggleForceCompositing(); + + // We've now made sure that |func|'s inactivity will be reflected as of + // this sample. We need to clear its HasChanged() flag so that it won't + // trigger this same clause in future samples (until it changes again). + func.ClearHasChanged(); + } + aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample(); +} + +static inline bool +IsTransformAttribute(int32_t aNamespaceID, nsIAtom *aAttributeName) +{ + return aNamespaceID == kNameSpaceID_None && + (aAttributeName == nsGkAtoms::transform || + aAttributeName == nsGkAtoms::patternTransform || + aAttributeName == nsGkAtoms::gradientTransform); +} + +// Helper function that, given a SVGAnimationElement, looks up its target +// element & target attribute and populates a nsSMILTargetIdentifier +// for this target. +/*static*/ bool +nsSMILAnimationController::GetTargetIdentifierForAnimation( + SVGAnimationElement* aAnimElem, nsSMILTargetIdentifier& aResult) +{ + // Look up target (animated) element + Element* targetElem = aAnimElem->GetTargetElementContent(); + if (!targetElem) + // Animation has no target elem -- skip it. + return false; + + // Look up target (animated) attribute + // SMILANIM section 3.1, attributeName may + // have an XMLNS prefix to indicate the XML namespace. + nsCOMPtr<nsIAtom> attributeName; + int32_t attributeNamespaceID; + if (!aAnimElem->GetTargetAttributeName(&attributeNamespaceID, + getter_AddRefs(attributeName))) + // Animation has no target attr -- skip it. + return false; + + // animateTransform can only animate transforms, conversely transforms + // can only be animated by animateTransform + if (IsTransformAttribute(attributeNamespaceID, attributeName) != + (aAnimElem->IsSVGElement(nsGkAtoms::animateTransform))) + return false; + + // Look up target (animated) attribute-type + nsSMILTargetAttrType attributeType = aAnimElem->GetTargetAttributeType(); + + // Check if an 'auto' attributeType refers to a CSS property or XML attribute. + // Note that SMIL requires we search for CSS properties first. So if they + // overlap, 'auto' = 'CSS'. (SMILANIM 3.1) + bool isCSS = false; + if (attributeType == eSMILTargetAttrType_auto) { + if (attributeNamespaceID == kNameSpaceID_None) { + // width/height are special as they may be attributes or for + // outer-<svg> elements, mapped into style. + if (attributeName == nsGkAtoms::width || + attributeName == nsGkAtoms::height) { + isCSS = targetElem->GetNameSpaceID() != kNameSpaceID_SVG; + } else { + nsCSSPropertyID prop = + nsCSSProps::LookupProperty(nsDependentAtomString(attributeName), + CSSEnabledState::eForAllContent); + isCSS = nsSMILCSSProperty::IsPropertyAnimatable(prop); + } + } + } else { + isCSS = (attributeType == eSMILTargetAttrType_CSS); + } + + // Construct the key + aResult.mElement = targetElem; + aResult.mAttributeName = attributeName; + aResult.mAttributeNamespaceID = attributeNamespaceID; + aResult.mIsCSS = isCSS; + + return true; +} + +void +nsSMILAnimationController::AddStyleUpdatesTo(RestyleTracker& aTracker) +{ + MOZ_ASSERT(mMightHavePendingStyleUpdates, + "Should only add style updates when we think we might have some"); + + for (auto iter = mAnimationElementTable.Iter(); !iter.Done(); iter.Next()) { + SVGAnimationElement* animElement = iter.Get()->GetKey(); + + nsSMILTargetIdentifier key; + if (!GetTargetIdentifierForAnimation(animElement, key)) { + // Something's wrong/missing about animation's target; skip this animation + continue; + } + + // mIsCSS true means that the rules are the ones returned from + // Element::GetSMILOverrideStyleDeclaration (via nsSMILCSSProperty objects), + // and mIsCSS false means the rules are nsSMILMappedAttribute objects + // returned from nsSVGElement::GetAnimatedContentStyleRule. + nsRestyleHint rshint = key.mIsCSS ? eRestyle_StyleAttribute_Animations + : eRestyle_SVGAttrAnimations; + aTracker.AddPendingRestyle(key.mElement, rshint, nsChangeHint(0)); + } + + mMightHavePendingStyleUpdates = false; +} + +//---------------------------------------------------------------------- +// Add/remove child time containers + +nsresult +nsSMILAnimationController::AddChild(nsSMILTimeContainer& aChild) +{ + TimeContainerPtrKey* key = mChildContainerTable.PutEntry(&aChild); + NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY); + + if (!mPauseState && mChildContainerTable.Count() == 1) { + MaybeStartSampling(GetRefreshDriver()); + Sample(); // Run the first sample manually + } + + return NS_OK; +} + +void +nsSMILAnimationController::RemoveChild(nsSMILTimeContainer& aChild) +{ + mChildContainerTable.RemoveEntry(&aChild); + + if (!mPauseState && mChildContainerTable.Count() == 0) { + StopSampling(GetRefreshDriver()); + } +} + +// Helper method +nsRefreshDriver* +nsSMILAnimationController::GetRefreshDriver() +{ + if (!mDocument) { + NS_ERROR("Requesting refresh driver after document has disconnected!"); + return nullptr; + } + + nsIPresShell* shell = mDocument->GetShell(); + if (!shell) { + return nullptr; + } + + nsPresContext* context = shell->GetPresContext(); + return context ? context->RefreshDriver() : nullptr; +} + +void +nsSMILAnimationController::FlagDocumentNeedsFlush() +{ + mDocument->SetNeedStyleFlush(); +} diff --git a/dom/smil/nsSMILAnimationController.h b/dom/smil/nsSMILAnimationController.h new file mode 100644 index 000000000..9c565b78b --- /dev/null +++ b/dom/smil/nsSMILAnimationController.h @@ -0,0 +1,211 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILANIMATIONCONTROLLER_H_ +#define NS_SMILANIMATIONCONTROLLER_H_ + +#include "mozilla/Attributes.h" +#include "nsAutoPtr.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsITimer.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsSMILTimeContainer.h" +#include "nsSMILCompositorTable.h" +#include "nsSMILMilestone.h" +#include "nsRefreshDriver.h" + +struct nsSMILTargetIdentifier; +class nsIDocument; + +namespace mozilla { +class RestyleTracker; +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +//---------------------------------------------------------------------- +// nsSMILAnimationController +// +// The animation controller maintains the animation timer and determines the +// sample times and sample rate for all SMIL animations in a document. There is +// at most one animation controller per nsDocument so that frame-rate tuning can +// be performed at a document-level. +// +// The animation controller can contain many child time containers (timed +// document root objects) which may correspond to SVG document fragments within +// a compound document. These time containers can be paused individually or +// here, at the document level. +// +class nsSMILAnimationController final : public nsSMILTimeContainer, + public nsARefreshObserver +{ +public: + explicit nsSMILAnimationController(nsIDocument* aDoc); + + // Clears mDocument pointer. (Called by our nsIDocument when it's going away) + void Disconnect(); + + // nsSMILContainer + virtual void Pause(uint32_t aType) override; + virtual void Resume(uint32_t aType) override; + virtual nsSMILTime GetParentTime() const override; + + // nsARefreshObserver + NS_IMETHOD_(MozExternalRefCountType) AddRef() override; + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + virtual void WillRefresh(mozilla::TimeStamp aTime) override; + + // Methods for registering and enumerating animation elements + void RegisterAnimationElement(mozilla::dom::SVGAnimationElement* aAnimationElement); + void UnregisterAnimationElement(mozilla::dom::SVGAnimationElement* aAnimationElement); + + // Methods for resampling all animations + // (A resample performs the same operations as a sample but doesn't advance + // the current time and doesn't check if the container is paused) + // This will flush pending style changes for the document. + void Resample() { DoSample(false); } + + void SetResampleNeeded() + { + if (!mRunningSample && !mResampleNeeded) { + FlagDocumentNeedsFlush(); + mResampleNeeded = true; + } + } + + // This will flush pending style changes for the document. + void FlushResampleRequests() + { + if (!mResampleNeeded) + return; + + Resample(); + } + + // Methods for handling page transitions + void OnPageShow(); + void OnPageHide(); + + // Methods for supporting cycle-collection + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + // Methods for relaying the availability of the refresh driver + void NotifyRefreshDriverCreated(nsRefreshDriver* aRefreshDriver); + void NotifyRefreshDriverDestroying(nsRefreshDriver* aRefreshDriver); + + // Helper to check if we have any animation elements at all + bool HasRegisteredAnimations() const + { + return mAnimationElementTable.Count() != 0; + } + + void AddStyleUpdatesTo(mozilla::RestyleTracker& aTracker); + bool MightHavePendingStyleUpdates() const + { + return mMightHavePendingStyleUpdates; + } + +protected: + ~nsSMILAnimationController(); + + // Typedefs + typedef nsPtrHashKey<nsSMILTimeContainer> TimeContainerPtrKey; + typedef nsTHashtable<TimeContainerPtrKey> TimeContainerHashtable; + typedef nsPtrHashKey<mozilla::dom::SVGAnimationElement> AnimationElementPtrKey; + typedef nsTHashtable<AnimationElementPtrKey> AnimationElementHashtable; + + // Returns mDocument's refresh driver, if it's got one. + nsRefreshDriver* GetRefreshDriver(); + + // Methods for controlling whether we're sampling + void StartSampling(nsRefreshDriver* aRefreshDriver); + void StopSampling(nsRefreshDriver* aRefreshDriver); + + // Wrapper for StartSampling that defers if no animations are registered. + void MaybeStartSampling(nsRefreshDriver* aRefreshDriver); + + // Sample-related callbacks and implementation helpers + virtual void DoSample() override; + void DoSample(bool aSkipUnchangedContainers); + + void RewindElements(); + + void DoMilestoneSamples(); + + static void SampleTimedElement(mozilla::dom::SVGAnimationElement* aElement, + TimeContainerHashtable* aActiveContainers); + + static void AddAnimationToCompositorTable( + mozilla::dom::SVGAnimationElement* aElement, + nsSMILCompositorTable* aCompositorTable, + bool& aStyleFlushNeeded); + + static bool GetTargetIdentifierForAnimation( + mozilla::dom::SVGAnimationElement* aAnimElem, nsSMILTargetIdentifier& aResult); + + // Methods for adding/removing time containers + virtual nsresult AddChild(nsSMILTimeContainer& aChild) override; + virtual void RemoveChild(nsSMILTimeContainer& aChild) override; + + void FlagDocumentNeedsFlush(); + + // Members + nsAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + AnimationElementHashtable mAnimationElementTable; + TimeContainerHashtable mChildContainerTable; + mozilla::TimeStamp mCurrentSampleTime; + mozilla::TimeStamp mStartTime; + + // Average time between samples from the refresh driver. This is used to + // detect large unexpected gaps between samples such as can occur when the + // computer sleeps. The nature of the SMIL model means that catching up these + // large gaps can be expensive as, for example, many events may need to be + // dispatched for the intervening time when no samples were received. + // + // In such cases, we ignore the intervening gap and continue sampling from + // when we were expecting the next sample to arrive. + // + // Note that we only do this for SMIL and not CSS transitions (which doesn't + // have so much work to do to catch up) nor scripted animations (which expect + // animation time to follow real time). + // + // This behaviour does not affect pausing (since we're not *expecting* any + // samples then) nor seeking (where the SMIL model behaves somewhat + // differently such as not dispatching events). + nsSMILTime mAvgTimeBetweenSamples; + + bool mResampleNeeded; + // If we're told to start sampling but there are no animation elements we just + // record the time, set the following flag, and then wait until we have an + // animation element. Then we'll reset this flag and actually start sampling. + bool mDeferredStartSampling; + bool mRunningSample; + + // Are we registered with our document's refresh driver? + bool mRegisteredWithRefreshDriver; + + // Have we updated animated values without adding them to the restyle tracker? + bool mMightHavePendingStyleUpdates; + + // Store raw ptr to mDocument. It owns the controller, so controller + // shouldn't outlive it + nsIDocument* mDocument; + + // Contains compositors used in our last sample. We keep this around + // so we can detect when an element/attribute used to be animated, + // but isn't anymore for some reason. (e.g. if its <animate> element is + // removed or retargeted) + nsAutoPtr<nsSMILCompositorTable> mLastCompositorTable; +}; + +#endif // NS_SMILANIMATIONCONTROLLER_H_ diff --git a/dom/smil/nsSMILAnimationFunction.cpp b/dom/smil/nsSMILAnimationFunction.cpp new file mode 100644 index 000000000..767181897 --- /dev/null +++ b/dom/smil/nsSMILAnimationFunction.cpp @@ -0,0 +1,1070 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILAnimationFunction.h" + +#include "mozilla/dom/SVGAnimationElement.h" +#include "mozilla/Move.h" +#include "nsISMILAttr.h" +#include "nsSMILCSSValueType.h" +#include "nsSMILParserUtils.h" +#include "nsSMILNullType.h" +#include "nsSMILTimedElement.h" +#include "nsAttrValueInlines.h" +#include "nsGkAtoms.h" +#include "nsCOMPtr.h" +#include "nsCOMArray.h" +#include "nsIContent.h" +#include "nsContentUtils.h" +#include "nsReadableUtils.h" +#include "nsString.h" +#include <math.h> +#include <algorithm> + +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// Static members + +nsAttrValue::EnumTable nsSMILAnimationFunction::sAccumulateTable[] = { + {"none", false}, + {"sum", true}, + {nullptr, 0} +}; + +nsAttrValue::EnumTable nsSMILAnimationFunction::sAdditiveTable[] = { + {"replace", false}, + {"sum", true}, + {nullptr, 0} +}; + +nsAttrValue::EnumTable nsSMILAnimationFunction::sCalcModeTable[] = { + {"linear", CALC_LINEAR}, + {"discrete", CALC_DISCRETE}, + {"paced", CALC_PACED}, + {"spline", CALC_SPLINE}, + {nullptr, 0} +}; + +// Any negative number should be fine as a sentinel here, +// because valid distances are non-negative. +#define COMPUTE_DISTANCE_ERROR (-1) + +//---------------------------------------------------------------------- +// Constructors etc. + +nsSMILAnimationFunction::nsSMILAnimationFunction() + : mSampleTime(-1), + mRepeatIteration(0), + mBeginTime(INT64_MIN), + mAnimationElement(nullptr), + mErrorFlags(0), + mIsActive(false), + mIsFrozen(false), + mLastValue(false), + mHasChanged(true), + mValueNeedsReparsingEverySample(false), + mPrevSampleWasSingleValueAnimation(false), + mWasSkippedInPrevSample(false) +{ +} + +void +nsSMILAnimationFunction::SetAnimationElement( + SVGAnimationElement* aAnimationElement) +{ + mAnimationElement = aAnimationElement; +} + +bool +nsSMILAnimationFunction::SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, nsresult* aParseResult) +{ + bool foundMatch = true; + nsresult parseResult = NS_OK; + + // The attributes 'by', 'from', 'to', and 'values' may be parsed differently + // depending on the element & attribute we're animating. So instead of + // parsing them now we re-parse them at every sample. + if (aAttribute == nsGkAtoms::by || + aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || + aAttribute == nsGkAtoms::values) { + // We parse to, from, by, values at sample time. + // XXX Need to flag which attribute has changed and then when we parse it at + // sample time, report any errors and reset the flag + mHasChanged = true; + aResult.SetTo(aValue); + } else if (aAttribute == nsGkAtoms::accumulate) { + parseResult = SetAccumulate(aValue, aResult); + } else if (aAttribute == nsGkAtoms::additive) { + parseResult = SetAdditive(aValue, aResult); + } else if (aAttribute == nsGkAtoms::calcMode) { + parseResult = SetCalcMode(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keyTimes) { + parseResult = SetKeyTimes(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keySplines) { + parseResult = SetKeySplines(aValue, aResult); + } else { + foundMatch = false; + } + + if (foundMatch && aParseResult) { + *aParseResult = parseResult; + } + + return foundMatch; +} + +bool +nsSMILAnimationFunction::UnsetAttr(nsIAtom* aAttribute) +{ + bool foundMatch = true; + + if (aAttribute == nsGkAtoms::by || + aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || + aAttribute == nsGkAtoms::values) { + mHasChanged = true; + } else if (aAttribute == nsGkAtoms::accumulate) { + UnsetAccumulate(); + } else if (aAttribute == nsGkAtoms::additive) { + UnsetAdditive(); + } else if (aAttribute == nsGkAtoms::calcMode) { + UnsetCalcMode(); + } else if (aAttribute == nsGkAtoms::keyTimes) { + UnsetKeyTimes(); + } else if (aAttribute == nsGkAtoms::keySplines) { + UnsetKeySplines(); + } else { + foundMatch = false; + } + + return foundMatch; +} + +void +nsSMILAnimationFunction::SampleAt(nsSMILTime aSampleTime, + const nsSMILTimeValue& aSimpleDuration, + uint32_t aRepeatIteration) +{ + // * Update mHasChanged ("Might this sample be different from prev one?") + // Were we previously sampling a fill="freeze" final val? (We're not anymore.) + mHasChanged |= mLastValue; + + // Are we sampling at a new point in simple duration? And does that matter? + mHasChanged |= + (mSampleTime != aSampleTime || mSimpleDuration != aSimpleDuration) && + !IsValueFixedForSimpleDuration(); + + // Are we on a new repeat and accumulating across repeats? + if (!mErrorFlags) { // (can't call GetAccumulate() if we've had parse errors) + mHasChanged |= (mRepeatIteration != aRepeatIteration) && GetAccumulate(); + } + + mSampleTime = aSampleTime; + mSimpleDuration = aSimpleDuration; + mRepeatIteration = aRepeatIteration; + mLastValue = false; +} + +void +nsSMILAnimationFunction::SampleLastValue(uint32_t aRepeatIteration) +{ + if (mHasChanged || !mLastValue || mRepeatIteration != aRepeatIteration) { + mHasChanged = true; + } + + mRepeatIteration = aRepeatIteration; + mLastValue = true; +} + +void +nsSMILAnimationFunction::Activate(nsSMILTime aBeginTime) +{ + mBeginTime = aBeginTime; + mIsActive = true; + mIsFrozen = false; + mHasChanged = true; +} + +void +nsSMILAnimationFunction::Inactivate(bool aIsFrozen) +{ + mIsActive = false; + mIsFrozen = aIsFrozen; + mHasChanged = true; +} + +void +nsSMILAnimationFunction::ComposeResult(const nsISMILAttr& aSMILAttr, + nsSMILValue& aResult) +{ + mHasChanged = false; + mPrevSampleWasSingleValueAnimation = false; + mWasSkippedInPrevSample = false; + + // Skip animations that are inactive or in error + if (!IsActiveOrFrozen() || mErrorFlags != 0) + return; + + // Get the animation values + nsSMILValueArray values; + nsresult rv = GetValues(aSMILAttr, values); + if (NS_FAILED(rv)) + return; + + // Check that we have the right number of keySplines and keyTimes + CheckValueListDependentAttrs(values.Length()); + if (mErrorFlags != 0) + return; + + // If this interval is active, we must have a non-negative mSampleTime + MOZ_ASSERT(mSampleTime >= 0 || !mIsActive, + "Negative sample time for active animation"); + MOZ_ASSERT(mSimpleDuration.IsResolved() || mLastValue, + "Unresolved simple duration for active or frozen animation"); + + // If we want to add but don't have a base value then just fail outright. + // This can happen when we skipped getting the base value because there's an + // animation function in the sandwich that should replace it but that function + // failed unexpectedly. + bool isAdditive = IsAdditive(); + if (isAdditive && aResult.IsNull()) + return; + + nsSMILValue result; + + if (values.Length() == 1 && !IsToAnimation()) { + + // Single-valued animation + result = values[0]; + mPrevSampleWasSingleValueAnimation = true; + + } else if (mLastValue) { + + // Sampling last value + const nsSMILValue& last = values[values.Length() - 1]; + result = last; + + // See comment in AccumulateResult: to-animation does not accumulate + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + // If the target attribute type doesn't support addition Add will + // fail leaving result = last + result.Add(last, mRepeatIteration); + } + + } else { + + // Interpolation + if (NS_FAILED(InterpolateResult(values, result, aResult))) + return; + + if (NS_FAILED(AccumulateResult(values, result))) + return; + } + + // If additive animation isn't required or isn't supported, set the value. + if (!isAdditive || NS_FAILED(aResult.SandwichAdd(result))) { + aResult = Move(result); + } +} + +int8_t +nsSMILAnimationFunction::CompareTo(const nsSMILAnimationFunction* aOther) const +{ + NS_ENSURE_TRUE(aOther, 0); + + NS_ASSERTION(aOther != this, "Trying to compare to self"); + + // Inactive animations sort first + if (!IsActiveOrFrozen() && aOther->IsActiveOrFrozen()) + return -1; + + if (IsActiveOrFrozen() && !aOther->IsActiveOrFrozen()) + return 1; + + // Sort based on begin time + if (mBeginTime != aOther->GetBeginTime()) + return mBeginTime > aOther->GetBeginTime() ? 1 : -1; + + // Next sort based on syncbase dependencies: the dependent element sorts after + // its syncbase + const nsSMILTimedElement& thisTimedElement = + mAnimationElement->TimedElement(); + const nsSMILTimedElement& otherTimedElement = + aOther->mAnimationElement->TimedElement(); + if (thisTimedElement.IsTimeDependent(otherTimedElement)) + return 1; + if (otherTimedElement.IsTimeDependent(thisTimedElement)) + return -1; + + // Animations that appear later in the document sort after those earlier in + // the document + MOZ_ASSERT(mAnimationElement != aOther->mAnimationElement, + "Two animations cannot have the same animation content element!"); + + return (nsContentUtils::PositionIsBefore(mAnimationElement, aOther->mAnimationElement)) + ? -1 : 1; +} + +bool +nsSMILAnimationFunction::WillReplace() const +{ + /* + * In IsAdditive() we don't consider to-animation to be additive as it is + * a special case that is dealt with differently in the compositing method. + * Here, however, we return FALSE for to-animation (i.e. it will NOT replace + * the underlying value) as it builds on the underlying value. + */ + return !mErrorFlags && !(IsAdditive() || IsToAnimation()); +} + +bool +nsSMILAnimationFunction::HasChanged() const +{ + return mHasChanged || mValueNeedsReparsingEverySample; +} + +bool +nsSMILAnimationFunction::UpdateCachedTarget( + const nsSMILTargetIdentifier& aNewTarget) +{ + if (!mLastTarget.Equals(aNewTarget)) { + mLastTarget = aNewTarget; + return true; + } + return false; +} + +//---------------------------------------------------------------------- +// Implementation helpers + +nsresult +nsSMILAnimationFunction::InterpolateResult(const nsSMILValueArray& aValues, + nsSMILValue& aResult, + nsSMILValue& aBaseValue) +{ + // Sanity check animation values + if ((!IsToAnimation() && aValues.Length() < 2) || + (IsToAnimation() && aValues.Length() != 1)) { + NS_ERROR("Unexpected number of values"); + return NS_ERROR_FAILURE; + } + + if (IsToAnimation() && aBaseValue.IsNull()) { + return NS_ERROR_FAILURE; + } + + // Get the normalised progress through the simple duration. + // + // If we have an indefinite simple duration, just set the progress to be + // 0 which will give us the expected behaviour of the animation being fixed at + // its starting point. + double simpleProgress = 0.0; + + if (mSimpleDuration.IsDefinite()) { + nsSMILTime dur = mSimpleDuration.GetMillis(); + + MOZ_ASSERT(dur >= 0, "Simple duration should not be negative"); + MOZ_ASSERT(mSampleTime >= 0, "Sample time should not be negative"); + + if (mSampleTime >= dur || mSampleTime < 0) { + NS_ERROR("Animation sampled outside interval"); + return NS_ERROR_FAILURE; + } + + if (dur > 0) { + simpleProgress = (double)mSampleTime / dur; + } // else leave simpleProgress at 0.0 (e.g. if mSampleTime == dur == 0) + } + + nsresult rv = NS_OK; + nsSMILCalcMode calcMode = GetCalcMode(); + + // Force discrete calcMode for visibility since StyleAnimationValue will + // try to interpolate it using the special clamping behavior defined for + // CSS. + if (nsSMILCSSValueType::PropertyFromValue(aValues[0]) + == eCSSProperty_visibility) { + calcMode = CALC_DISCRETE; + } + + if (calcMode != CALC_DISCRETE) { + // Get the normalised progress between adjacent values + const nsSMILValue* from = nullptr; + const nsSMILValue* to = nullptr; + // Init to -1 to make sure that if we ever forget to set this, the + // MOZ_ASSERT that tests that intervalProgress is in range will fail. + double intervalProgress = -1.f; + if (IsToAnimation()) { + from = &aBaseValue; + to = &aValues[0]; + if (calcMode == CALC_PACED) { + // Note: key[Times/Splines/Points] are ignored for calcMode="paced" + intervalProgress = simpleProgress; + } else { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + intervalProgress = ScaleIntervalProgress(scaledSimpleProgress, 0); + } + } else if (calcMode == CALC_PACED) { + rv = ComputePacedPosition(aValues, simpleProgress, + intervalProgress, from, to); + // Note: If the above call fails, we'll skip the "from->Interpolate" + // call below, and we'll drop into the CALC_DISCRETE section + // instead. (as the spec says we should, because our failure was + // presumably due to the values being non-additive) + } else { // calcMode == CALC_LINEAR or calcMode == CALC_SPLINE + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + uint32_t index = (uint32_t)floor(scaledSimpleProgress * + (aValues.Length() - 1)); + from = &aValues[index]; + to = &aValues[index + 1]; + intervalProgress = + scaledSimpleProgress * (aValues.Length() - 1) - index; + intervalProgress = ScaleIntervalProgress(intervalProgress, index); + } + + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(from, "NULL from-value during interpolation"); + MOZ_ASSERT(to, "NULL to-value during interpolation"); + MOZ_ASSERT(0.0f <= intervalProgress && intervalProgress < 1.0f, + "Interval progress should be in the range [0, 1)"); + rv = from->Interpolate(*to, intervalProgress, aResult); + } + } + + // Discrete-CalcMode case + // Note: If interpolation failed (isn't supported for this type), the SVG + // spec says to force discrete mode. + if (calcMode == CALC_DISCRETE || NS_FAILED(rv)) { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, CALC_DISCRETE); + + // Floating-point errors can mean that, for example, a sample time of 29s in + // a 100s duration animation gives us a simple progress of 0.28999999999 + // instead of the 0.29 we'd expect. Normally this isn't a noticeable + // problem, but when we have sudden jumps in animation values (such as is + // the case here with discrete animation) we can get unexpected results. + // + // To counteract this, before we perform a floor() on the animation + // progress, we add a tiny fudge factor to push us into the correct interval + // in cases where floating-point errors might cause us to fall short. + static const double kFloatingPointFudgeFactor = 1.0e-16; + if (scaledSimpleProgress + kFloatingPointFudgeFactor <= 1.0) { + scaledSimpleProgress += kFloatingPointFudgeFactor; + } + + if (IsToAnimation()) { + // We don't follow SMIL 3, 12.6.4, where discrete to animations + // are the same as <set> animations. Instead, we treat it as a + // discrete animation with two values (the underlying value and + // the to="" value), and honor keyTimes="" as well. + uint32_t index = (uint32_t)floor(scaledSimpleProgress * 2); + aResult = index == 0 ? aBaseValue : aValues[0]; + } else { + uint32_t index = (uint32_t)floor(scaledSimpleProgress * aValues.Length()); + aResult = aValues[index]; + } + rv = NS_OK; + } + return rv; +} + +nsresult +nsSMILAnimationFunction::AccumulateResult(const nsSMILValueArray& aValues, + nsSMILValue& aResult) +{ + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + const nsSMILValue& lastValue = aValues[aValues.Length() - 1]; + + // If the target attribute type doesn't support addition, Add will + // fail and we leave aResult untouched. + aResult.Add(lastValue, mRepeatIteration); + } + + return NS_OK; +} + +/* + * Given the simple progress for a paced animation, this method: + * - determines which two elements of the values array we're in between + * (returned as aFrom and aTo) + * - determines where we are between them + * (returned as aIntervalProgress) + * + * Returns NS_OK, or NS_ERROR_FAILURE if our values don't support distance + * computation. + */ +nsresult +nsSMILAnimationFunction::ComputePacedPosition(const nsSMILValueArray& aValues, + double aSimpleProgress, + double& aIntervalProgress, + const nsSMILValue*& aFrom, + const nsSMILValue*& aTo) +{ + NS_ASSERTION(0.0f <= aSimpleProgress && aSimpleProgress < 1.0f, + "aSimpleProgress is out of bounds"); + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + MOZ_ASSERT(aValues.Length() >= 2, "Unexpected number of values"); + + // Trivial case: If we have just 2 values, then there's only one interval + // for us to traverse, and our progress across that interval is the exact + // same as our overall progress. + if (aValues.Length() == 2) { + aIntervalProgress = aSimpleProgress; + aFrom = &aValues[0]; + aTo = &aValues[1]; + return NS_OK; + } + + double totalDistance = ComputePacedTotalDistance(aValues); + if (totalDistance == COMPUTE_DISTANCE_ERROR) + return NS_ERROR_FAILURE; + + // If we have 0 total distance, then it's unclear where our "paced" position + // should be. We can just fail, which drops us into discrete animation mode. + // (That's fine, since our values are apparently indistinguishable anyway.) + if (totalDistance == 0.0) { + return NS_ERROR_FAILURE; + } + + // total distance we should have moved at this point in time. + // (called 'remainingDist' due to how it's used in loop below) + double remainingDist = aSimpleProgress * totalDistance; + + // Must be satisfied, because totalDistance is a sum of (non-negative) + // distances, and aSimpleProgress is non-negative + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + // Find where remainingDist puts us in the list of values + // Note: We could optimize this next loop by caching the + // interval-distances in an array, but maybe that's excessive. + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + // Note: The following assertion is valid because remainingDist should + // start out non-negative, and this loop never shaves off more than its + // current value. + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + double curIntervalDist; + +#ifdef DEBUG + nsresult rv = +#endif + aValues[i].ComputeDistance(aValues[i+1], curIntervalDist); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "If we got through ComputePacedTotalDistance, we should " + "be able to recompute each sub-distance without errors"); + + NS_ASSERTION(curIntervalDist >= 0, "distance values must be non-negative"); + // Clamp distance value at 0, just in case ComputeDistance is evil. + curIntervalDist = std::max(curIntervalDist, 0.0); + + if (remainingDist >= curIntervalDist) { + remainingDist -= curIntervalDist; + } else { + // NOTE: If we get here, then curIntervalDist necessarily is not 0. Why? + // Because this clause is only hit when remainingDist < curIntervalDist, + // and if curIntervalDist were 0, that would mean remainingDist would + // have to be < 0. But that can't happen, because remainingDist (as + // a distance) is non-negative by definition. + NS_ASSERTION(curIntervalDist != 0, + "We should never get here with this set to 0..."); + + // We found the right spot -- an interpolated position between + // values i and i+1. + aFrom = &aValues[i]; + aTo = &aValues[i+1]; + aIntervalProgress = remainingDist / curIntervalDist; + return NS_OK; + } + } + + NS_NOTREACHED("shouldn't complete loop & get here -- if we do, " + "then aSimpleProgress was probably out of bounds"); + return NS_ERROR_FAILURE; +} + +/* + * Computes the total distance to be travelled by a paced animation. + * + * Returns the total distance, or returns COMPUTE_DISTANCE_ERROR if + * our values don't support distance computation. + */ +double +nsSMILAnimationFunction::ComputePacedTotalDistance( + const nsSMILValueArray& aValues) const +{ + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + + double totalDistance = 0.0; + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + double tmpDist; + nsresult rv = aValues[i].ComputeDistance(aValues[i+1], tmpDist); + if (NS_FAILED(rv)) { + return COMPUTE_DISTANCE_ERROR; + } + + // Clamp distance value to 0, just in case we have an evil ComputeDistance + // implementation somewhere + MOZ_ASSERT(tmpDist >= 0.0f, "distance values must be non-negative"); + tmpDist = std::max(tmpDist, 0.0); + + totalDistance += tmpDist; + } + + return totalDistance; +} + +double +nsSMILAnimationFunction::ScaleSimpleProgress(double aProgress, + nsSMILCalcMode aCalcMode) +{ + if (!HasAttr(nsGkAtoms::keyTimes)) + return aProgress; + + uint32_t numTimes = mKeyTimes.Length(); + + if (numTimes < 2) + return aProgress; + + uint32_t i = 0; + for (; i < numTimes - 2 && aProgress >= mKeyTimes[i+1]; ++i) { } + + if (aCalcMode == CALC_DISCRETE) { + // discrete calcMode behaviour differs in that each keyTime defines the time + // from when the corresponding value is set, and therefore the last value + // needn't be 1. So check if we're in the last 'interval', that is, the + // space between the final value and 1.0. + if (aProgress >= mKeyTimes[i+1]) { + MOZ_ASSERT(i == numTimes - 2, + "aProgress is not in range of the current interval, yet the " + "current interval is not the last bounded interval either."); + ++i; + } + return (double)i / numTimes; + } + + double& intervalStart = mKeyTimes[i]; + double& intervalEnd = mKeyTimes[i+1]; + + double intervalLength = intervalEnd - intervalStart; + if (intervalLength <= 0.0) + return intervalStart; + + return (i + (aProgress - intervalStart) / intervalLength) / + double(numTimes - 1); +} + +double +nsSMILAnimationFunction::ScaleIntervalProgress(double aProgress, + uint32_t aIntervalIndex) +{ + if (GetCalcMode() != CALC_SPLINE) + return aProgress; + + if (!HasAttr(nsGkAtoms::keySplines)) + return aProgress; + + MOZ_ASSERT(aIntervalIndex < mKeySplines.Length(), + "Invalid interval index"); + + nsSMILKeySpline const &spline = mKeySplines[aIntervalIndex]; + return spline.GetSplineValue(aProgress); +} + +bool +nsSMILAnimationFunction::HasAttr(nsIAtom* aAttName) const +{ + return mAnimationElement->HasAnimAttr(aAttName); +} + +const nsAttrValue* +nsSMILAnimationFunction::GetAttr(nsIAtom* aAttName) const +{ + return mAnimationElement->GetAnimAttr(aAttName); +} + +bool +nsSMILAnimationFunction::GetAttr(nsIAtom* aAttName, nsAString& aResult) const +{ + return mAnimationElement->GetAnimAttr(aAttName, aResult); +} + +/* + * A utility function to make querying an attribute that corresponds to an + * nsSMILValue a little neater. + * + * @param aAttName The attribute name (in the global namespace). + * @param aSMILAttr The SMIL attribute to perform the parsing. + * @param[out] aResult The resulting nsSMILValue. + * @param[out] aPreventCachingOfSandwich + * If |aResult| contains dependencies on its context that + * should prevent the result of the animation sandwich from + * being cached and reused in future samples (as reported + * by nsISMILAttr::ValueFromString), then this outparam + * will be set to true. Otherwise it is left unmodified. + * + * Returns false if a parse error occurred, otherwise returns true. + */ +bool +nsSMILAnimationFunction::ParseAttr(nsIAtom* aAttName, + const nsISMILAttr& aSMILAttr, + nsSMILValue& aResult, + bool& aPreventCachingOfSandwich) const +{ + nsAutoString attValue; + if (GetAttr(aAttName, attValue)) { + bool preventCachingOfSandwich = false; + nsresult rv = aSMILAttr.ValueFromString(attValue, mAnimationElement, + aResult, preventCachingOfSandwich); + if (NS_FAILED(rv)) + return false; + + if (preventCachingOfSandwich) { + aPreventCachingOfSandwich = true; + } + } + return true; +} + +/* + * SMILANIM specifies the following rules for animation function values: + * + * (1) if values is set, it overrides everything + * (2) for from/to/by animation at least to or by must be specified, from on its + * own (or nothing) is an error--which we will ignore + * (3) if both by and to are specified only to will be used, by will be ignored + * (4) if by is specified without from (by animation), forces additive behaviour + * (5) if to is specified without from (to animation), special care needs to be + * taken when compositing animation as such animations are composited last. + * + * This helper method applies these rules to fill in the values list and to set + * some internal state. + */ +nsresult +nsSMILAnimationFunction::GetValues(const nsISMILAttr& aSMILAttr, + nsSMILValueArray& aResult) +{ + if (!mAnimationElement) + return NS_ERROR_FAILURE; + + mValueNeedsReparsingEverySample = false; + nsSMILValueArray result; + + // If "values" is set, use it + if (HasAttr(nsGkAtoms::values)) { + nsAutoString attValue; + GetAttr(nsGkAtoms::values, attValue); + bool preventCachingOfSandwich = false; + if (!nsSMILParserUtils::ParseValues(attValue, mAnimationElement, + aSMILAttr, result, + preventCachingOfSandwich)) { + return NS_ERROR_FAILURE; + } + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + // Else try to/from/by + } else { + bool preventCachingOfSandwich = false; + bool parseOk = true; + nsSMILValue to, from, by; + parseOk &= ParseAttr(nsGkAtoms::to, aSMILAttr, to, + preventCachingOfSandwich); + parseOk &= ParseAttr(nsGkAtoms::from, aSMILAttr, from, + preventCachingOfSandwich); + parseOk &= ParseAttr(nsGkAtoms::by, aSMILAttr, by, + preventCachingOfSandwich); + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + + if (!parseOk || !result.SetCapacity(2, mozilla::fallible)) { + return NS_ERROR_FAILURE; + } + + // AppendElement() below must succeed, because SetCapacity() succeeded. + if (!to.IsNull()) { + if (!from.IsNull()) { + MOZ_ALWAYS_TRUE(result.AppendElement(from, mozilla::fallible)); + MOZ_ALWAYS_TRUE(result.AppendElement(to, mozilla::fallible)); + } else { + MOZ_ALWAYS_TRUE(result.AppendElement(to, mozilla::fallible)); + } + } else if (!by.IsNull()) { + nsSMILValue effectiveFrom(by.mType); + if (!from.IsNull()) + effectiveFrom = from; + // Set values to 'from; from + by' + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveFrom, mozilla::fallible)); + nsSMILValue effectiveTo(effectiveFrom); + if (!effectiveTo.IsNull() && NS_SUCCEEDED(effectiveTo.Add(by))) { + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveTo, mozilla::fallible)); + } else { + // Using by-animation with non-additive type or bad base-value + return NS_ERROR_FAILURE; + } + } else { + // No values, no to, no by -- call it a day + return NS_ERROR_FAILURE; + } + } + + result.SwapElements(aResult); + + return NS_OK; +} + +void +nsSMILAnimationFunction::CheckValueListDependentAttrs(uint32_t aNumValues) +{ + CheckKeyTimes(aNumValues); + CheckKeySplines(aNumValues); +} + +/** + * Performs checks for the keyTimes attribute required by the SMIL spec but + * which depend on other attributes and therefore needs to be updated as + * dependent attributes are set. + */ +void +nsSMILAnimationFunction::CheckKeyTimes(uint32_t aNumValues) +{ + if (!HasAttr(nsGkAtoms::keyTimes)) + return; + + nsSMILCalcMode calcMode = GetCalcMode(); + + // attribute is ignored for calcMode = paced + if (calcMode == CALC_PACED) { + SetKeyTimesErrorFlag(false); + return; + } + + uint32_t numKeyTimes = mKeyTimes.Length(); + if (numKeyTimes < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeyTimesErrorFlag(true); + return; + } + + // no. keyTimes == no. values + // For to-animation the number of values is considered to be 2. + bool matchingNumOfValues = + numKeyTimes == (IsToAnimation() ? 2 : aNumValues); + if (!matchingNumOfValues) { + SetKeyTimesErrorFlag(true); + return; + } + + // first value must be 0 + if (mKeyTimes[0] != 0.0) { + SetKeyTimesErrorFlag(true); + return; + } + + // last value must be 1 for linear or spline calcModes + if (calcMode != CALC_DISCRETE && numKeyTimes > 1 && + mKeyTimes[numKeyTimes - 1] != 1.0) { + SetKeyTimesErrorFlag(true); + return; + } + + SetKeyTimesErrorFlag(false); +} + +void +nsSMILAnimationFunction::CheckKeySplines(uint32_t aNumValues) +{ + // attribute is ignored if calc mode is not spline + if (GetCalcMode() != CALC_SPLINE) { + SetKeySplinesErrorFlag(false); + return; + } + + // calc mode is spline but the attribute is not set + if (!HasAttr(nsGkAtoms::keySplines)) { + SetKeySplinesErrorFlag(false); + return; + } + + if (mKeySplines.Length() < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeySplinesErrorFlag(true); + return; + } + + // ignore splines if there's only one value + if (aNumValues == 1 && !IsToAnimation()) { + SetKeySplinesErrorFlag(false); + return; + } + + // no. keySpline specs == no. values - 1 + uint32_t splineSpecs = mKeySplines.Length(); + if ((splineSpecs != aNumValues - 1 && !IsToAnimation()) || + (IsToAnimation() && splineSpecs != 1)) { + SetKeySplinesErrorFlag(true); + return; + } + + SetKeySplinesErrorFlag(false); +} + +bool +nsSMILAnimationFunction::IsValueFixedForSimpleDuration() const +{ + return mSimpleDuration.IsIndefinite() || + (!mHasChanged && mPrevSampleWasSingleValueAnimation); +} + +//---------------------------------------------------------------------- +// Property getters + +bool +nsSMILAnimationFunction::GetAccumulate() const +{ + const nsAttrValue* value = GetAttr(nsGkAtoms::accumulate); + if (!value) + return false; + + return value->GetEnumValue(); +} + +bool +nsSMILAnimationFunction::GetAdditive() const +{ + const nsAttrValue* value = GetAttr(nsGkAtoms::additive); + if (!value) + return false; + + return value->GetEnumValue(); +} + +nsSMILAnimationFunction::nsSMILCalcMode +nsSMILAnimationFunction::GetCalcMode() const +{ + const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode); + if (!value) + return CALC_LINEAR; + + return nsSMILCalcMode(value->GetEnumValue()); +} + +//---------------------------------------------------------------------- +// Property setters / un-setters: + +nsresult +nsSMILAnimationFunction::SetAccumulate(const nsAString& aAccumulate, + nsAttrValue& aResult) +{ + mHasChanged = true; + bool parseResult = + aResult.ParseEnumValue(aAccumulate, sAccumulateTable, true); + SetAccumulateErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILAnimationFunction::UnsetAccumulate() +{ + SetAccumulateErrorFlag(false); + mHasChanged = true; +} + +nsresult +nsSMILAnimationFunction::SetAdditive(const nsAString& aAdditive, + nsAttrValue& aResult) +{ + mHasChanged = true; + bool parseResult + = aResult.ParseEnumValue(aAdditive, sAdditiveTable, true); + SetAdditiveErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILAnimationFunction::UnsetAdditive() +{ + SetAdditiveErrorFlag(false); + mHasChanged = true; +} + +nsresult +nsSMILAnimationFunction::SetCalcMode(const nsAString& aCalcMode, + nsAttrValue& aResult) +{ + mHasChanged = true; + bool parseResult + = aResult.ParseEnumValue(aCalcMode, sCalcModeTable, true); + SetCalcModeErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILAnimationFunction::UnsetCalcMode() +{ + SetCalcModeErrorFlag(false); + mHasChanged = true; +} + +nsresult +nsSMILAnimationFunction::SetKeySplines(const nsAString& aKeySplines, + nsAttrValue& aResult) +{ + mKeySplines.Clear(); + aResult.SetTo(aKeySplines); + + mHasChanged = true; + + if (!nsSMILParserUtils::ParseKeySplines(aKeySplines, mKeySplines)) { + mKeySplines.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void +nsSMILAnimationFunction::UnsetKeySplines() +{ + mKeySplines.Clear(); + SetKeySplinesErrorFlag(false); + mHasChanged = true; +} + +nsresult +nsSMILAnimationFunction::SetKeyTimes(const nsAString& aKeyTimes, + nsAttrValue& aResult) +{ + mKeyTimes.Clear(); + aResult.SetTo(aKeyTimes); + + mHasChanged = true; + + if (!nsSMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyTimes, true, + mKeyTimes)) { + mKeyTimes.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void +nsSMILAnimationFunction::UnsetKeyTimes() +{ + mKeyTimes.Clear(); + SetKeyTimesErrorFlag(false); + mHasChanged = true; +} diff --git a/dom/smil/nsSMILAnimationFunction.h b/dom/smil/nsSMILAnimationFunction.h new file mode 100644 index 000000000..2380b64ef --- /dev/null +++ b/dom/smil/nsSMILAnimationFunction.h @@ -0,0 +1,458 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILANIMATIONFUNCTION_H_ +#define NS_SMILANIMATIONFUNCTION_H_ + +#include "nsISMILAttr.h" +#include "nsGkAtoms.h" +#include "nsString.h" +#include "nsSMILTargetIdentifier.h" +#include "nsSMILTimeValue.h" +#include "nsSMILKeySpline.h" +#include "nsSMILValue.h" +#include "nsTArray.h" +#include "nsAttrValue.h" +#include "nsSMILTypes.h" + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +//---------------------------------------------------------------------- +// nsSMILAnimationFunction +// +// The animation function calculates animation values. It it is provided with +// time parameters (sample time, repeat iteration etc.) and it uses this to +// build an appropriate animation value by performing interpolation and +// addition operations. +// +// It is responsible for implementing the animation parameters of an animation +// element (e.g. from, by, to, values, calcMode, additive, accumulate, keyTimes, +// keySplines) +// +class nsSMILAnimationFunction +{ +public: + nsSMILAnimationFunction(); + + /* + * Sets the owning animation element which this class uses to query attribute + * values and compare document positions. + */ + void SetAnimationElement(mozilla::dom::SVGAnimationElement* aAnimationElement); + + /* + * Sets animation-specific attributes (or marks them dirty, in the case + * of from/to/by/values). + * + * @param aAttribute The attribute being set + * @param aValue The updated value of the attribute. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aParseResult Outparam used for reporting parse errors. Will be set + * to NS_OK if everything succeeds. + * @return true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, nsresult* aParseResult = nullptr); + + /* + * Unsets the given attribute. + * + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool UnsetAttr(nsIAtom* aAttribute); + + /** + * Indicate a new sample has occurred. + * + * @param aSampleTime The sample time for this timed element expressed in + * simple time. + * @param aSimpleDuration The simple duration for this timed element. + * @param aRepeatIteration The repeat iteration for this sample. The first + * iteration has a value of 0. + */ + void SampleAt(nsSMILTime aSampleTime, + const nsSMILTimeValue& aSimpleDuration, + uint32_t aRepeatIteration); + + /** + * Indicate to sample using the last value defined for the animation function. + * This value is not normally sampled due to the end-point exclusive timing + * model but only occurs when the fill mode is "freeze" and the active + * duration is an even multiple of the simple duration. + * + * @param aRepeatIteration The repeat iteration for this sample. The first + * iteration has a value of 0. + */ + void SampleLastValue(uint32_t aRepeatIteration); + + /** + * Indicate that this animation is now active. This is used to instruct the + * animation function that it should now add its result to the animation + * sandwich. The begin time is also provided for proper prioritization of + * animation functions, and for this reason, this method must be called + * before either of the Sample methods. + * + * @param aBeginTime The begin time for the newly active interval. + */ + void Activate(nsSMILTime aBeginTime); + + /** + * Indicate that this animation is no longer active. This is used to instruct + * the animation function that it should no longer add its result to the + * animation sandwich. + * + * @param aIsFrozen true if this animation should continue to contribute + * to the animation sandwich using the most recent sample + * parameters. + */ + void Inactivate(bool aIsFrozen); + + /** + * Combines the result of this animation function for the last sample with the + * specified value. + * + * @param aSMILAttr This animation's target attribute. Used here for + * doing attribute-specific parsing of from/to/by/values. + * + * @param aResult The value to compose with. + */ + void ComposeResult(const nsISMILAttr& aSMILAttr, nsSMILValue& aResult); + + /** + * Returns the relative priority of this animation to another. The priority is + * used for determining the position of the animation in the animation + * sandwich -- higher priority animations are applied on top of lower + * priority animations. + * + * @return -1 if this animation has lower priority or 1 if this animation has + * higher priority + * + * This method should never return any other value, including 0. + */ + int8_t CompareTo(const nsSMILAnimationFunction* aOther) const; + + /* + * The following methods are provided so that the compositor can optimize its + * operations by only composing those animation that will affect the final + * result. + */ + + /** + * Indicates if the animation is currently active or frozen. Inactive + * animations will not contribute to the composed result. + * + * @return true if the animation is active or frozen, false otherwise. + */ + bool IsActiveOrFrozen() const + { + /* + * - Frozen animations should be considered active for the purposes of + * compositing. + * - This function does not assume that our nsSMILValues (by/from/to/values) + * have already been parsed. + */ + return (mIsActive || mIsFrozen); + } + + /** + * Indicates if the animation is active. + * + * @return true if the animation is active, false otherwise. + */ + bool IsActive() const { + return mIsActive; + } + + /** + * Indicates if this animation will replace the passed in result rather than + * adding to it. Animations that replace the underlying value may be called + * without first calling lower priority animations. + * + * @return True if the animation will replace, false if it will add or + * otherwise build on the passed in value. + */ + virtual bool WillReplace() const; + + /** + * Indicates if the parameters for this animation have changed since the last + * time it was composited. This allows rendering to be performed only when + * necessary, particularly when no animations are active. + * + * Note that the caller is responsible for determining if the animation + * target has changed (with help from my UpdateCachedTarget() method). + * + * @return true if the animation parameters have changed, false + * otherwise. + */ + bool HasChanged() const; + + /** + * This method lets us clear the 'HasChanged' flag for inactive animations + * after we've reacted to their change to the 'inactive' state, so that we + * won't needlessly recompose their targets in every sample. + * + * This should only be called on an animation function that is inactive and + * that returns true from HasChanged(). + */ + void ClearHasChanged() + { + MOZ_ASSERT(HasChanged(), + "clearing mHasChanged flag, when it's already false"); + MOZ_ASSERT(!IsActiveOrFrozen(), + "clearing mHasChanged flag for active animation"); + mHasChanged = false; + } + + /** + * Updates the cached record of our animation target, and returns a boolean + * that indicates whether the target has changed since the last call to this + * function. (This lets nsSMILCompositor check whether its animation + * functions have changed value or target since the last sample. If none of + * them have, then the compositor doesn't need to do anything.) + * + * @param aNewTarget A nsSMILTargetIdentifier representing the animation + * target of this function for this sample. + * @return true if |aNewTarget| is different from the old cached value; + * otherwise, false. + */ + bool UpdateCachedTarget(const nsSMILTargetIdentifier& aNewTarget); + + /** + * Returns true if this function was skipped in the previous sample (because + * there was a higher-priority non-additive animation). If a skipped animation + * function is later used, then the animation sandwich must be recomposited. + */ + bool WasSkippedInPrevSample() const { + return mWasSkippedInPrevSample; + } + + /** + * Mark this animation function as having been skipped. By marking the + * function as skipped, if it is used in a subsequent sample we'll know to + * recomposite the sandwich. + */ + void SetWasSkipped() { + mWasSkippedInPrevSample = true; + } + + /** + * Returns true if we need to recalculate the animation value on every sample. + * (e.g. because it depends on context like the font-size) + */ + bool ValueNeedsReparsingEverySample() const { + return mValueNeedsReparsingEverySample; + } + + // Comparator utility class, used for sorting nsSMILAnimationFunctions + class Comparator { + public: + bool Equals(const nsSMILAnimationFunction* aElem1, + const nsSMILAnimationFunction* aElem2) const { + return (aElem1->CompareTo(aElem2) == 0); + } + bool LessThan(const nsSMILAnimationFunction* aElem1, + const nsSMILAnimationFunction* aElem2) const { + return (aElem1->CompareTo(aElem2) < 0); + } + }; + +protected: + // Typedefs + typedef FallibleTArray<nsSMILValue> nsSMILValueArray; + + // Types + enum nsSMILCalcMode : uint8_t + { + CALC_LINEAR, + CALC_DISCRETE, + CALC_PACED, + CALC_SPLINE + }; + + // Used for sorting nsSMILAnimationFunctions + nsSMILTime GetBeginTime() const { return mBeginTime; } + + // Property getters + bool GetAccumulate() const; + bool GetAdditive() const; + virtual nsSMILCalcMode GetCalcMode() const; + + // Property setters + nsresult SetAccumulate(const nsAString& aAccumulate, nsAttrValue& aResult); + nsresult SetAdditive(const nsAString& aAdditive, nsAttrValue& aResult); + nsresult SetCalcMode(const nsAString& aCalcMode, nsAttrValue& aResult); + nsresult SetKeyTimes(const nsAString& aKeyTimes, nsAttrValue& aResult); + nsresult SetKeySplines(const nsAString& aKeySplines, nsAttrValue& aResult); + + // Property un-setters + void UnsetAccumulate(); + void UnsetAdditive(); + void UnsetCalcMode(); + void UnsetKeyTimes(); + void UnsetKeySplines(); + + // Helpers + virtual nsresult InterpolateResult(const nsSMILValueArray& aValues, + nsSMILValue& aResult, + nsSMILValue& aBaseValue); + nsresult AccumulateResult(const nsSMILValueArray& aValues, + nsSMILValue& aResult); + + nsresult ComputePacedPosition(const nsSMILValueArray& aValues, + double aSimpleProgress, + double& aIntervalProgress, + const nsSMILValue*& aFrom, + const nsSMILValue*& aTo); + double ComputePacedTotalDistance(const nsSMILValueArray& aValues) const; + + /** + * Adjust the simple progress, that is, the point within the simple duration, + * by applying any keyTimes. + */ + double ScaleSimpleProgress(double aProgress, nsSMILCalcMode aCalcMode); + /** + * Adjust the progress within an interval, that is, between two animation + * values, by applying any keySplines. + */ + double ScaleIntervalProgress(double aProgress, uint32_t aIntervalIndex); + + // Convenience attribute getters -- use these instead of querying + // mAnimationElement as these may need to be overridden by subclasses + virtual bool HasAttr(nsIAtom* aAttName) const; + virtual const nsAttrValue* GetAttr(nsIAtom* aAttName) const; + virtual bool GetAttr(nsIAtom* aAttName, + nsAString& aResult) const; + + bool ParseAttr(nsIAtom* aAttName, const nsISMILAttr& aSMILAttr, + nsSMILValue& aResult, + bool& aPreventCachingOfSandwich) const; + + virtual nsresult GetValues(const nsISMILAttr& aSMILAttr, + nsSMILValueArray& aResult); + + virtual void CheckValueListDependentAttrs(uint32_t aNumValues); + void CheckKeyTimes(uint32_t aNumValues); + void CheckKeySplines(uint32_t aNumValues); + + virtual bool IsToAnimation() const { + return !HasAttr(nsGkAtoms::values) && + HasAttr(nsGkAtoms::to) && + !HasAttr(nsGkAtoms::from); + } + + // Returns true if we know our composited value won't change over the + // simple duration of this animation (for a fixed base value). + virtual bool IsValueFixedForSimpleDuration() const; + + inline bool IsAdditive() const { + /* + * Animation is additive if: + * + * (1) additive = "sum" (GetAdditive() == true), or + * (2) it is 'by animation' (by is set, from and values are not) + * + * Although animation is not additive if it is 'to animation' + */ + bool isByAnimation = (!HasAttr(nsGkAtoms::values) && + HasAttr(nsGkAtoms::by) && + !HasAttr(nsGkAtoms::from)); + return !IsToAnimation() && (GetAdditive() || isByAnimation); + } + + // Setters for error flags + // These correspond to bit-indices in mErrorFlags, for tracking parse errors + // in these attributes, when those parse errors should block us from doing + // animation. + enum AnimationAttributeIdx { + BF_ACCUMULATE = 0, + BF_ADDITIVE = 1, + BF_CALC_MODE = 2, + BF_KEY_TIMES = 3, + BF_KEY_SPLINES = 4, + BF_KEY_POINTS = 5 // <animateMotion> only + }; + + inline void SetAccumulateErrorFlag(bool aNewValue) { + SetErrorFlag(BF_ACCUMULATE, aNewValue); + } + inline void SetAdditiveErrorFlag(bool aNewValue) { + SetErrorFlag(BF_ADDITIVE, aNewValue); + } + inline void SetCalcModeErrorFlag(bool aNewValue) { + SetErrorFlag(BF_CALC_MODE, aNewValue); + } + inline void SetKeyTimesErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_TIMES, aNewValue); + } + inline void SetKeySplinesErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_SPLINES, aNewValue); + } + inline void SetKeyPointsErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_POINTS, aNewValue); + } + inline void SetErrorFlag(AnimationAttributeIdx aField, bool aValue) { + if (aValue) { + mErrorFlags |= (0x01 << aField); + } else { + mErrorFlags &= ~(0x01 << aField); + } + } + + // Members + // ------- + + static nsAttrValue::EnumTable sAdditiveTable[]; + static nsAttrValue::EnumTable sCalcModeTable[]; + static nsAttrValue::EnumTable sAccumulateTable[]; + + FallibleTArray<double> mKeyTimes; + FallibleTArray<nsSMILKeySpline> mKeySplines; + + // These are the parameters provided by the previous sample. Currently we + // perform lazy calculation. That is, we only calculate the result if and when + // instructed by the compositor. This allows us to apply the result directly + // to the animation value and allows the compositor to filter out functions + // that it determines will not contribute to the final result. + nsSMILTime mSampleTime; // sample time within simple dur + nsSMILTimeValue mSimpleDuration; + uint32_t mRepeatIteration; + + nsSMILTime mBeginTime; // document time + + // The owning animation element. This is used for sorting based on document + // position and for fetching attribute values stored in the element. + // Raw pointer is OK here, because this nsSMILAnimationFunction can't outlive + // its owning animation element. + mozilla::dom::SVGAnimationElement* mAnimationElement; + + // Which attributes have been set but have had errors. This is not used for + // all attributes but only those which have specified error behaviour + // associated with them. + uint16_t mErrorFlags; + + // Allows us to check whether an animation function has changed target from + // sample to sample (because if neither target nor animated value have + // changed, we don't have to do anything). + nsSMILWeakTargetIdentifier mLastTarget; + + // Boolean flags + bool mIsActive:1; + bool mIsFrozen:1; + bool mLastValue:1; + bool mHasChanged:1; + bool mValueNeedsReparsingEverySample:1; + bool mPrevSampleWasSingleValueAnimation:1; + bool mWasSkippedInPrevSample:1; +}; + +#endif // NS_SMILANIMATIONFUNCTION_H_ diff --git a/dom/smil/nsSMILCSSProperty.cpp b/dom/smil/nsSMILCSSProperty.cpp new file mode 100644 index 000000000..53f3e0fbf --- /dev/null +++ b/dom/smil/nsSMILCSSProperty.cpp @@ -0,0 +1,275 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a SMIL-animatable CSS property on an element */ + +#include "nsSMILCSSProperty.h" + +#include "mozilla/dom/Element.h" +#include "mozilla/Move.h" +#include "nsSMILCSSValueType.h" +#include "nsSMILValue.h" +#include "nsComputedDOMStyle.h" +#include "nsCSSProps.h" +#include "nsIDOMElement.h" +#include "nsIDocument.h" + +using namespace mozilla::dom; + +// Helper function +static bool +GetCSSComputedValue(Element* aElem, + nsCSSPropertyID aPropID, + nsAString& aResult) +{ + MOZ_ASSERT(!nsCSSProps::IsShorthand(aPropID), + "Can't look up computed value of shorthand property"); + MOZ_ASSERT(nsSMILCSSProperty::IsPropertyAnimatable(aPropID), + "Shouldn't get here for non-animatable properties"); + + nsIDocument* doc = aElem->GetUncomposedDoc(); + if (!doc) { + // This can happen if we process certain types of restyles mid-sample + // and remove anonymous animated content from the document as a result. + // See bug 534975. + return false; + } + + nsIPresShell* shell = doc->GetShell(); + if (!shell) { + NS_WARNING("Unable to look up computed style -- no pres shell"); + return false; + } + + RefPtr<nsComputedDOMStyle> computedStyle = + NS_NewComputedDOMStyle(aElem, EmptyString(), shell); + + computedStyle->GetPropertyValue(aPropID, aResult); + return true; +} + +// Class Methods +nsSMILCSSProperty::nsSMILCSSProperty(nsCSSPropertyID aPropID, + Element* aElement) + : mPropID(aPropID), mElement(aElement) +{ + MOZ_ASSERT(IsPropertyAnimatable(mPropID), + "Creating a nsSMILCSSProperty for a property " + "that's not supported for animation"); +} + +nsSMILValue +nsSMILCSSProperty::GetBaseValue() const +{ + // To benefit from Return Value Optimization and avoid copy constructor calls + // due to our use of return-by-value, we must return the exact same object + // from ALL return points. This function must only return THIS variable: + nsSMILValue baseValue; + + // SPECIAL CASE: (a) Shorthands + // (b) 'display' + if (nsCSSProps::IsShorthand(mPropID) || mPropID == eCSSProperty_display) { + // We can't look up the base (computed-style) value of shorthand + // properties because they aren't guaranteed to have a consistent computed + // value. + // + // Also, although we can look up the base value of the display property, + // doing so involves clearing and resetting the property which can cause + // frames to be recreated which we'd like to avoid. + // + // In either case, just return a dummy value (initialized with the right + // type, so as not to indicate failure). + nsSMILValue tmpVal(&nsSMILCSSValueType::sSingleton); + Swap(baseValue, tmpVal); + return baseValue; + } + + // GENERAL CASE: Non-Shorthands + // (1) Put empty string in override style for property mPropID + // (saving old override style value, so we can set it again when we're done) + nsICSSDeclaration* overrideDecl = mElement->GetSMILOverrideStyle(); + nsAutoString cachedOverrideStyleVal; + if (overrideDecl) { + overrideDecl->GetPropertyValue(mPropID, cachedOverrideStyleVal); + // (Don't bother clearing override style if it's already empty) + if (!cachedOverrideStyleVal.IsEmpty()) { + overrideDecl->SetPropertyValue(mPropID, EmptyString()); + } + } + + // (2) Get Computed Style + nsAutoString computedStyleVal; + bool didGetComputedVal = GetCSSComputedValue(mElement, mPropID, + computedStyleVal); + + // (3) Put cached override style back (if it's non-empty) + if (overrideDecl && !cachedOverrideStyleVal.IsEmpty()) { + overrideDecl->SetPropertyValue(mPropID, cachedOverrideStyleVal); + } + + // (4) Populate our nsSMILValue from the computed style + if (didGetComputedVal) { + // When we parse animation values we check if they are context-sensitive or + // not so that we don't cache animation values whose meaning may change. + // For base values however this is unnecessary since on each sample the + // compositor will fetch the (computed) base value and compare it against + // the cached (computed) value and detect changes for us. + nsSMILCSSValueType::ValueFromString(mPropID, mElement, + computedStyleVal, baseValue, + nullptr); + } + return baseValue; +} + +nsresult +nsSMILCSSProperty::ValueFromString(const nsAString& aStr, + const SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const +{ + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + nsSMILCSSValueType::ValueFromString(mPropID, mElement, aStr, aValue, + &aPreventCachingOfSandwich); + + if (aValue.IsNull()) { + return NS_ERROR_FAILURE; + } + + // XXX Due to bug 536660 (or at least that seems to be the most likely + // culprit), when we have animation setting display:none on a <use> element, + // if we DON'T set the property every sample, chaos ensues. + if (!aPreventCachingOfSandwich && mPropID == eCSSProperty_display) { + aPreventCachingOfSandwich = true; + } + return NS_OK; +} + +nsresult +nsSMILCSSProperty::SetAnimValue(const nsSMILValue& aValue) +{ + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + // Convert nsSMILValue to string + nsAutoString valStr; + if (!nsSMILCSSValueType::ValueToString(aValue, valStr)) { + NS_WARNING("Failed to convert nsSMILValue for CSS property into a string"); + return NS_ERROR_FAILURE; + } + + // Use string value to style the target element + nsICSSDeclaration* overrideDecl = mElement->GetSMILOverrideStyle(); + if (overrideDecl) { + nsAutoString oldValStr; + overrideDecl->GetPropertyValue(mPropID, oldValStr); + if (valStr.Equals(oldValStr)) { + return NS_OK; + } + overrideDecl->SetPropertyValue(mPropID, valStr); + } + return NS_OK; +} + +void +nsSMILCSSProperty::ClearAnimValue() +{ + // Put empty string in override style for our property + nsICSSDeclaration* overrideDecl = mElement->GetSMILOverrideStyle(); + if (overrideDecl) { + overrideDecl->SetPropertyValue(mPropID, EmptyString()); + } +} + +// Based on http://www.w3.org/TR/SVG/propidx.html +// static +bool +nsSMILCSSProperty::IsPropertyAnimatable(nsCSSPropertyID aPropID) +{ + // NOTE: Right now, Gecko doesn't recognize the following properties from + // the SVG Property Index: + // alignment-baseline + // baseline-shift + // color-profile + // color-rendering + // glyph-orientation-horizontal + // glyph-orientation-vertical + // kerning + // writing-mode + + switch (aPropID) { + case eCSSProperty_clip: + case eCSSProperty_clip_rule: + case eCSSProperty_clip_path: + case eCSSProperty_color: + case eCSSProperty_color_interpolation: + case eCSSProperty_color_interpolation_filters: + case eCSSProperty_cursor: + case eCSSProperty_display: + case eCSSProperty_dominant_baseline: + case eCSSProperty_fill: + case eCSSProperty_fill_opacity: + case eCSSProperty_fill_rule: + case eCSSProperty_filter: + case eCSSProperty_flood_color: + case eCSSProperty_flood_opacity: + case eCSSProperty_font: + case eCSSProperty_font_family: + case eCSSProperty_font_size: + case eCSSProperty_font_size_adjust: + case eCSSProperty_font_stretch: + case eCSSProperty_font_style: + case eCSSProperty_font_variant: + case eCSSProperty_font_weight: + case eCSSProperty_height: + case eCSSProperty_image_rendering: + case eCSSProperty_letter_spacing: + case eCSSProperty_lighting_color: + case eCSSProperty_marker: + case eCSSProperty_marker_end: + case eCSSProperty_marker_mid: + case eCSSProperty_marker_start: + case eCSSProperty_mask: + case eCSSProperty_mask_type: + case eCSSProperty_opacity: + case eCSSProperty_overflow: + case eCSSProperty_pointer_events: + case eCSSProperty_shape_rendering: + case eCSSProperty_stop_color: + case eCSSProperty_stop_opacity: + case eCSSProperty_stroke: + case eCSSProperty_stroke_dasharray: + case eCSSProperty_stroke_dashoffset: + case eCSSProperty_stroke_linecap: + case eCSSProperty_stroke_linejoin: + case eCSSProperty_stroke_miterlimit: + case eCSSProperty_stroke_opacity: + case eCSSProperty_stroke_width: + case eCSSProperty_text_anchor: + case eCSSProperty_text_decoration: + case eCSSProperty_text_decoration_line: + case eCSSProperty_text_rendering: + case eCSSProperty_vector_effect: + case eCSSProperty_width: + case eCSSProperty_visibility: + case eCSSProperty_word_spacing: + return true; + + // EXPLICITLY NON-ANIMATABLE PROPERTIES: + // (Some of these aren't supported at all in Gecko -- I've commented those + // ones out. If/when we add support for them, uncomment their line here) + // ---------------------------------------------------------------------- + // case eCSSProperty_enable_background: + // case eCSSProperty_glyph_orientation_horizontal: + // case eCSSProperty_glyph_orientation_vertical: + // case eCSSProperty_writing_mode: + case eCSSProperty_direction: + case eCSSProperty_unicode_bidi: + return false; + + default: + return false; + } +} diff --git a/dom/smil/nsSMILCSSProperty.h b/dom/smil/nsSMILCSSProperty.h new file mode 100644 index 000000000..028a9aaa2 --- /dev/null +++ b/dom/smil/nsSMILCSSProperty.h @@ -0,0 +1,67 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a SMIL-animatable CSS property on an element */ + +#ifndef NS_SMILCSSPROPERTY_H_ +#define NS_SMILCSSPROPERTY_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILAttr.h" +#include "nsIAtom.h" +#include "nsCSSPropertyID.h" +#include "nsCSSValue.h" + +namespace mozilla { +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + +/** + * nsSMILCSSProperty: Implements the nsISMILAttr interface for SMIL animations + * that target CSS properties. Represents a particular animation-targeted CSS + * property on a particular element. + */ +class nsSMILCSSProperty : public nsISMILAttr +{ +public: + /** + * Constructs a new nsSMILCSSProperty. + * @param aPropID The CSS property we're interested in animating. + * @param aElement The element whose CSS property is being animated. + */ + nsSMILCSSProperty(nsCSSPropertyID aPropID, mozilla::dom::Element* aElement); + + // nsISMILAttr methods + virtual nsresult ValueFromString(const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const override; + virtual nsSMILValue GetBaseValue() const override; + virtual nsresult SetAnimValue(const nsSMILValue& aValue) override; + virtual void ClearAnimValue() override; + + /** + * Utility method - returns true if the given property is supported for + * SMIL animation. + * + * @param aProperty The property to check for animation support. + * @return true if the given property is supported for SMIL animation, or + * false otherwise + */ + static bool IsPropertyAnimatable(nsCSSPropertyID aPropID); + +protected: + nsCSSPropertyID mPropID; + // Using non-refcounted pointer for mElement -- we know mElement will stay + // alive for my lifetime because a nsISMILAttr (like me) only lives as long + // as the Compositing step, and DOM elements don't get a chance to die during + // that time. + mozilla::dom::Element* mElement; +}; + +#endif // NS_SMILCSSPROPERTY_H_ diff --git a/dom/smil/nsSMILCSSValueType.cpp b/dom/smil/nsSMILCSSValueType.cpp new file mode 100644 index 000000000..ed89e7710 --- /dev/null +++ b/dom/smil/nsSMILCSSValueType.cpp @@ -0,0 +1,447 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a value for a SMIL-animated CSS property */ + +#include "nsSMILCSSValueType.h" +#include "nsString.h" +#include "nsSMILParserUtils.h" +#include "nsSMILValue.h" +#include "nsCSSValue.h" +#include "nsColor.h" +#include "nsPresContext.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/dom/Element.h" +#include "nsDebug.h" +#include "nsStyleUtil.h" +#include "nsIDocument.h" + +using namespace mozilla::dom; +using mozilla::StyleAnimationValue; + +/*static*/ nsSMILCSSValueType nsSMILCSSValueType::sSingleton; + +struct ValueWrapper { + ValueWrapper(nsCSSPropertyID aPropID, const StyleAnimationValue& aValue) : + mPropID(aPropID), mCSSValue(aValue) {} + + nsCSSPropertyID mPropID; + StyleAnimationValue mCSSValue; +}; + +// Helper Methods +// -------------- +static const StyleAnimationValue* +GetZeroValueForUnit(StyleAnimationValue::Unit aUnit) +{ + static const StyleAnimationValue + sZeroCoord(0, StyleAnimationValue::CoordConstructor); + static const StyleAnimationValue + sZeroPercent(0.0f, StyleAnimationValue::PercentConstructor); + static const StyleAnimationValue + sZeroFloat(0.0f, StyleAnimationValue::FloatConstructor); + static const StyleAnimationValue + sZeroColor(NS_RGB(0,0,0), StyleAnimationValue::ColorConstructor); + + MOZ_ASSERT(aUnit != StyleAnimationValue::eUnit_Null, + "Need non-null unit for a zero value"); + switch (aUnit) { + case StyleAnimationValue::eUnit_Coord: + return &sZeroCoord; + case StyleAnimationValue::eUnit_Percent: + return &sZeroPercent; + case StyleAnimationValue::eUnit_Float: + return &sZeroFloat; + case StyleAnimationValue::eUnit_Color: + return &sZeroColor; + default: + return nullptr; + } +} + +// This method requires at least one of its arguments to be non-null. +// +// If one argument is null, this method updates it to point to "zero" +// for the other argument's Unit (if applicable; otherwise, we return false). +// +// If neither argument is null, this method generally does nothing, though it +// may apply a workaround for the special case where a 0 length-value is mixed +// with a eUnit_Float value. (See comment below.) +// +// Returns true on success, or false. +static bool +FinalizeStyleAnimationValues(const StyleAnimationValue*& aValue1, + const StyleAnimationValue*& aValue2) +{ + MOZ_ASSERT(aValue1 || aValue2, + "expecting at least one non-null value"); + + // Are we missing either val? (If so, it's an implied 0 in other val's units) + if (!aValue1) { + aValue1 = GetZeroValueForUnit(aValue2->GetUnit()); + return !!aValue1; // Fail if we have no zero value for this unit. + } + if (!aValue2) { + aValue2 = GetZeroValueForUnit(aValue1->GetUnit()); + return !!aValue2; // Fail if we have no zero value for this unit. + } + + // Ok, both values were specified. + // Need to handle a special-case, though: unitless nonzero length (parsed as + // eUnit_Float) mixed with unitless 0 length (parsed as eUnit_Coord). These + // won't interoperate in StyleAnimationValue, since their Units don't match. + // In this case, we replace the eUnit_Coord 0 value with eUnit_Float 0 value. + const StyleAnimationValue& zeroCoord = + *GetZeroValueForUnit(StyleAnimationValue::eUnit_Coord); + if (*aValue1 == zeroCoord && + aValue2->GetUnit() == StyleAnimationValue::eUnit_Float) { + aValue1 = GetZeroValueForUnit(StyleAnimationValue::eUnit_Float); + } else if (*aValue2 == zeroCoord && + aValue1->GetUnit() == StyleAnimationValue::eUnit_Float) { + aValue2 = GetZeroValueForUnit(StyleAnimationValue::eUnit_Float); + } + + return true; +} + +static void +InvertSign(StyleAnimationValue& aValue) +{ + switch (aValue.GetUnit()) { + case StyleAnimationValue::eUnit_Coord: + aValue.SetCoordValue(-aValue.GetCoordValue()); + break; + case StyleAnimationValue::eUnit_Percent: + aValue.SetPercentValue(-aValue.GetPercentValue()); + break; + case StyleAnimationValue::eUnit_Float: + aValue.SetFloatValue(-aValue.GetFloatValue()); + break; + default: + NS_NOTREACHED("Calling InvertSign with an unsupported unit"); + break; + } +} + +static ValueWrapper* +ExtractValueWrapper(nsSMILValue& aValue) +{ + return static_cast<ValueWrapper*>(aValue.mU.mPtr); +} + +static const ValueWrapper* +ExtractValueWrapper(const nsSMILValue& aValue) +{ + return static_cast<const ValueWrapper*>(aValue.mU.mPtr); +} + +// Class methods +// ------------- +void +nsSMILCSSValueType::Init(nsSMILValue& aValue) const +{ + MOZ_ASSERT(aValue.IsNull(), "Unexpected SMIL value type"); + + aValue.mU.mPtr = nullptr; + aValue.mType = this; +} + +void +nsSMILCSSValueType::Destroy(nsSMILValue& aValue) const +{ + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value type"); + delete static_cast<ValueWrapper*>(aValue.mU.mPtr); + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +nsSMILCSSValueType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value type"); + const ValueWrapper* srcWrapper = ExtractValueWrapper(aSrc); + ValueWrapper* destWrapper = ExtractValueWrapper(aDest); + + if (srcWrapper) { + if (!destWrapper) { + // barely-initialized dest -- need to alloc & copy + aDest.mU.mPtr = new ValueWrapper(*srcWrapper); + } else { + // both already fully-initialized -- just copy straight across + *destWrapper = *srcWrapper; + } + } else if (destWrapper) { + // fully-initialized dest, barely-initialized src -- clear dest + delete destWrapper; + aDest.mU.mPtr = destWrapper = nullptr; + } // else, both are barely-initialized -- nothing to do. + + return NS_OK; +} + +bool +nsSMILCSSValueType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected SMIL value"); + const ValueWrapper* leftWrapper = ExtractValueWrapper(aLeft); + const ValueWrapper* rightWrapper = ExtractValueWrapper(aRight); + + if (leftWrapper) { + if (rightWrapper) { + // Both non-null + NS_WARNING_ASSERTION(leftWrapper != rightWrapper, + "Two nsSMILValues with matching ValueWrapper ptr"); + return (leftWrapper->mPropID == rightWrapper->mPropID && + leftWrapper->mCSSValue == rightWrapper->mCSSValue); + } + // Left non-null, right null + return false; + } + if (rightWrapper) { + // Left null, right non-null + return false; + } + // Both null + return true; +} + +nsresult +nsSMILCSSValueType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + + ValueWrapper* destWrapper = ExtractValueWrapper(aDest); + const ValueWrapper* valueToAddWrapper = ExtractValueWrapper(aValueToAdd); + MOZ_ASSERT(destWrapper || valueToAddWrapper, + "need at least one fully-initialized value"); + + nsCSSPropertyID property = (valueToAddWrapper ? valueToAddWrapper->mPropID : + destWrapper->mPropID); + // Special case: font-size-adjust and stroke-dasharray are explicitly + // non-additive (even though StyleAnimationValue *could* support adding them) + if (property == eCSSProperty_font_size_adjust || + property == eCSSProperty_stroke_dasharray) { + return NS_ERROR_FAILURE; + } + + const StyleAnimationValue* valueToAdd = valueToAddWrapper ? + &valueToAddWrapper->mCSSValue : nullptr; + const StyleAnimationValue* destValue = destWrapper ? + &destWrapper->mCSSValue : nullptr; + if (!FinalizeStyleAnimationValues(valueToAdd, destValue)) { + return NS_ERROR_FAILURE; + } + // Did FinalizeStyleAnimationValues change destValue? + // If so, update outparam to use the new value. + if (destWrapper && &destWrapper->mCSSValue != destValue) { + destWrapper->mCSSValue = *destValue; + } + + // Handle barely-initialized "zero" destination. + if (!destWrapper) { + aDest.mU.mPtr = destWrapper = + new ValueWrapper(property, *destValue); + } + + return StyleAnimationValue::Add(property, + destWrapper->mCSSValue, *valueToAdd, aCount) ? + NS_OK : NS_ERROR_FAILURE; +} + +nsresult +nsSMILCSSValueType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + MOZ_ASSERT(aFrom.mType == aTo.mType, + "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + + const ValueWrapper* fromWrapper = ExtractValueWrapper(aFrom); + const ValueWrapper* toWrapper = ExtractValueWrapper(aTo); + MOZ_ASSERT(toWrapper, "expecting non-null endpoint"); + + const StyleAnimationValue* fromCSSValue = fromWrapper ? + &fromWrapper->mCSSValue : nullptr; + const StyleAnimationValue* toCSSValue = &toWrapper->mCSSValue; + if (!FinalizeStyleAnimationValues(fromCSSValue, toCSSValue)) { + return NS_ERROR_FAILURE; + } + + return StyleAnimationValue::ComputeDistance(toWrapper->mPropID, + *fromCSSValue, *toCSSValue, + nullptr, + aDistance) ? + NS_OK : NS_ERROR_FAILURE; +} + +nsresult +nsSMILCSSValueType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, + "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + MOZ_ASSERT(aUnitDistance >= 0.0 && aUnitDistance <= 1.0, + "unit distance value out of bounds"); + MOZ_ASSERT(!aResult.mU.mPtr, "expecting barely-initialized outparam"); + + const ValueWrapper* startWrapper = ExtractValueWrapper(aStartVal); + const ValueWrapper* endWrapper = ExtractValueWrapper(aEndVal); + MOZ_ASSERT(endWrapper, "expecting non-null endpoint"); + + const StyleAnimationValue* startCSSValue = startWrapper ? + &startWrapper->mCSSValue : nullptr; + const StyleAnimationValue* endCSSValue = &endWrapper->mCSSValue; + if (!FinalizeStyleAnimationValues(startCSSValue, endCSSValue)) { + return NS_ERROR_FAILURE; + } + + StyleAnimationValue resultValue; + if (StyleAnimationValue::Interpolate(endWrapper->mPropID, + *startCSSValue, *endCSSValue, + aUnitDistance, resultValue)) { + aResult.mU.mPtr = new ValueWrapper(endWrapper->mPropID, resultValue); + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +// Helper function to extract presContext +static nsPresContext* +GetPresContextForElement(Element* aElem) +{ + nsIDocument* doc = aElem->GetUncomposedDoc(); + if (!doc) { + // This can happen if we process certain types of restyles mid-sample + // and remove anonymous animated content from the document as a result. + // See bug 534975. + return nullptr; + } + nsIPresShell* shell = doc->GetShell(); + return shell ? shell->GetPresContext() : nullptr; +} + +// Helper function to parse a string into a StyleAnimationValue +static bool +ValueFromStringHelper(nsCSSPropertyID aPropID, + Element* aTargetElement, + nsPresContext* aPresContext, + const nsAString& aString, + StyleAnimationValue& aStyleAnimValue, + bool* aIsContextSensitive) +{ + // If value is negative, we'll strip off the "-" so the CSS parser won't + // barf, and then manually make the parsed value negative. + // (This is a partial solution to let us accept some otherwise out-of-bounds + // CSS values. Bug 501188 will provide a more complete fix.) + bool isNegative = false; + uint32_t subStringBegin = 0; + + // NOTE: We need to opt-out 'stroke-dasharray' from the negative-number + // check. Its values might look negative (e.g. by starting with "-1"), but + // they're more complicated than our simple negation logic here can handle. + if (aPropID != eCSSProperty_stroke_dasharray) { + int32_t absValuePos = nsSMILParserUtils::CheckForNegativeNumber(aString); + if (absValuePos > 0) { + isNegative = true; + subStringBegin = (uint32_t)absValuePos; // Start parsing after '-' sign + } + } + RefPtr<nsStyleContext> styleContext = + nsComputedDOMStyle::GetStyleContextForElement(aTargetElement, nullptr, + aPresContext->PresShell()); + if (!styleContext) { + return false; + } + nsDependentSubstring subString(aString, subStringBegin); + if (!StyleAnimationValue::ComputeValue(aPropID, aTargetElement, styleContext, + subString, true, aStyleAnimValue, + aIsContextSensitive)) { + return false; + } + if (isNegative) { + InvertSign(aStyleAnimValue); + } + + if (aPropID == eCSSProperty_font_size) { + // Divide out text-zoom, since SVG is supposed to ignore it + MOZ_ASSERT(aStyleAnimValue.GetUnit() == StyleAnimationValue::eUnit_Coord, + "'font-size' value with unexpected style unit"); + aStyleAnimValue.SetCoordValue(aStyleAnimValue.GetCoordValue() / + aPresContext->TextZoom()); + } + return true; +} + +// static +void +nsSMILCSSValueType::ValueFromString(nsCSSPropertyID aPropID, + Element* aTargetElement, + const nsAString& aString, + nsSMILValue& aValue, + bool* aIsContextSensitive) +{ + MOZ_ASSERT(aValue.IsNull(), "Outparam should be null-typed"); + nsPresContext* presContext = GetPresContextForElement(aTargetElement); + if (!presContext) { + NS_WARNING("Not parsing animation value; unable to get PresContext"); + return; + } + + nsIDocument* doc = aTargetElement->GetUncomposedDoc(); + if (doc && !nsStyleUtil::CSPAllowsInlineStyle(nullptr, + doc->NodePrincipal(), + doc->GetDocumentURI(), + 0, aString, nullptr)) { + return; + } + + StyleAnimationValue parsedValue; + if (ValueFromStringHelper(aPropID, aTargetElement, presContext, + aString, parsedValue, aIsContextSensitive)) { + sSingleton.Init(aValue); + aValue.mU.mPtr = new ValueWrapper(aPropID, parsedValue); + } +} + +// static +bool +nsSMILCSSValueType::ValueToString(const nsSMILValue& aValue, + nsAString& aString) +{ + MOZ_ASSERT(aValue.mType == &nsSMILCSSValueType::sSingleton, + "Unexpected SMIL value type"); + const ValueWrapper* wrapper = ExtractValueWrapper(aValue); + return !wrapper || + StyleAnimationValue::UncomputeValue(wrapper->mPropID, + wrapper->mCSSValue, aString); +} + +// static +nsCSSPropertyID +nsSMILCSSValueType::PropertyFromValue(const nsSMILValue& aValue) +{ + if (aValue.mType != &nsSMILCSSValueType::sSingleton) { + return eCSSProperty_UNKNOWN; + } + + const ValueWrapper* wrapper = ExtractValueWrapper(aValue); + if (!wrapper) { + return eCSSProperty_UNKNOWN; + } + + return wrapper->mPropID; +} diff --git a/dom/smil/nsSMILCSSValueType.h b/dom/smil/nsSMILCSSValueType.h new file mode 100644 index 000000000..0c71605f0 --- /dev/null +++ b/dom/smil/nsSMILCSSValueType.h @@ -0,0 +1,116 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a value for a SMIL-animated CSS property */ + +#ifndef NS_SMILCSSVALUETYPE_H_ +#define NS_SMILCSSVALUETYPE_H_ + +#include "nsISMILType.h" +#include "nsCSSPropertyID.h" +#include "mozilla/Attributes.h" + +class nsAString; + +namespace mozilla { +namespace dom { +class Element; +} // namespace dom +} // namespace mozilla + +/* + * nsSMILCSSValueType: Represents a SMIL-animated CSS value. + */ +class nsSMILCSSValueType : public nsISMILType +{ +public: + typedef mozilla::dom::Element Element; + + // Singleton for nsSMILValue objects to hold onto. + static nsSMILCSSValueType sSingleton; + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue&) const override; + virtual nsresult Assign(nsSMILValue& aDest, + const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, + const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +public: + // Helper Methods + // -------------- + /** + * Sets up the given nsSMILValue to represent the given string value. The + * string is interpreted as a value for the given property on the given + * element. + * + * On failure, this method leaves aValue.mType == nsSMILNullType::sSingleton. + * Otherwise, this method leaves aValue.mType == this class's singleton. + * + * @param aPropID The property for which we're parsing a value. + * @param aTargetElement The target element to whom the property/value + * setting applies. + * @param aString The string to be parsed as a CSS value. + * @param [out] aValue The nsSMILValue to be populated. Should + * initially be null-typed. + * @param [out] aIsContextSensitive Set to true if |aString| may produce + * a different |aValue| depending on other + * CSS properties on |aTargetElement| + * or its ancestors (e.g. 'inherit). + * false otherwise. May be nullptr. + * Not set if the method fails. + * @pre aValue.IsNull() + * @post aValue.IsNull() || aValue.mType == nsSMILCSSValueType::sSingleton + */ + static void ValueFromString(nsCSSPropertyID aPropID, + Element* aTargetElement, + const nsAString& aString, + nsSMILValue& aValue, + bool* aIsContextSensitive); + + /** + * Creates a string representation of the given nsSMILValue. + * + * Note: aValue is expected to be of this type (that is, it's expected to + * have been initialized by nsSMILCSSValueType::sSingleton). If aValue is a + * freshly-initialized value, this method will succeed, though the resulting + * string will be empty. + * + * @param aValue The nsSMILValue to be converted into a string. + * @param [out] aString The string to be populated with the given value. + * @return true on success, false on failure. + */ + static bool ValueToString(const nsSMILValue& aValue, nsAString& aString); + + /** + * Return the CSS property animated by the specified value. + * + * @param aValue The nsSMILValue to examine. + * @return The nsCSSPropertyID enum value of the property animated + * by |aValue|, or eCSSProperty_UNKNOWN if the type of + * |aValue| is not nsSMILCSSValueType. + */ + static nsCSSPropertyID PropertyFromValue(const nsSMILValue& aValue); + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr nsSMILCSSValueType() {} +}; + +#endif // NS_SMILCSSVALUETYPE_H_ diff --git a/dom/smil/nsSMILCompositor.cpp b/dom/smil/nsSMILCompositor.cpp new file mode 100644 index 000000000..fe7b2c828 --- /dev/null +++ b/dom/smil/nsSMILCompositor.cpp @@ -0,0 +1,204 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILCompositor.h" + +#include "nsCSSProps.h" +#include "nsHashKeys.h" +#include "nsSMILCSSProperty.h" + +// PLDHashEntryHdr methods +bool +nsSMILCompositor::KeyEquals(KeyTypePointer aKey) const +{ + return aKey && aKey->Equals(mKey); +} + +/*static*/ PLDHashNumber +nsSMILCompositor::HashKey(KeyTypePointer aKey) +{ + // Combine the 3 values into one numeric value, which will be hashed. + // NOTE: We right-shift one of the pointers by 2 to get some randomness in + // its 2 lowest-order bits. (Those shifted-off bits will always be 0 since + // our pointers will be word-aligned.) + return (NS_PTR_TO_UINT32(aKey->mElement.get()) >> 2) + + NS_PTR_TO_UINT32(aKey->mAttributeName.get()) + + (aKey->mIsCSS ? 1 : 0); +} + +// Cycle-collection support +void +nsSMILCompositor::Traverse(nsCycleCollectionTraversalCallback* aCallback) +{ + if (!mKey.mElement) + return; + + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCallback, "Compositor mKey.mElement"); + aCallback->NoteXPCOMChild(mKey.mElement); +} + +// Other methods +void +nsSMILCompositor::AddAnimationFunction(nsSMILAnimationFunction* aFunc) +{ + if (aFunc) { + mAnimationFunctions.AppendElement(aFunc); + } +} + +void +nsSMILCompositor::ComposeAttribute(bool& aMightHavePendingStyleUpdates) +{ + if (!mKey.mElement) + return; + + // FIRST: Get the nsISMILAttr (to grab base value from, and to eventually + // give animated value to) + nsAutoPtr<nsISMILAttr> smilAttr(CreateSMILAttr()); + if (!smilAttr) { + // Target attribute not found (or, out of memory) + return; + } + if (mAnimationFunctions.IsEmpty()) { + // No active animation functions. (We can still have a nsSMILCompositor in + // that case if an animation function has *just* become inactive) + smilAttr->ClearAnimValue(); + // Removing the animation effect may require a style update. + aMightHavePendingStyleUpdates = true; + return; + } + + // SECOND: Sort the animationFunctions, to prepare for compositing. + nsSMILAnimationFunction::Comparator comparator; + mAnimationFunctions.Sort(comparator); + + // THIRD: Step backwards through animation functions to find out + // which ones we actually care about. + uint32_t firstFuncToCompose = GetFirstFuncToAffectSandwich(); + + // FOURTH: Get & cache base value + nsSMILValue sandwichResultValue; + if (!mAnimationFunctions[firstFuncToCompose]->WillReplace()) { + sandwichResultValue = smilAttr->GetBaseValue(); + } + UpdateCachedBaseValue(sandwichResultValue); + + if (!mForceCompositing) { + return; + } + + // FIFTH: Compose animation functions + aMightHavePendingStyleUpdates = true; + uint32_t length = mAnimationFunctions.Length(); + for (uint32_t i = firstFuncToCompose; i < length; ++i) { + mAnimationFunctions[i]->ComposeResult(*smilAttr, sandwichResultValue); + } + if (sandwichResultValue.IsNull()) { + smilAttr->ClearAnimValue(); + return; + } + + // SIXTH: Set the animated value to the final composited result. + nsresult rv = smilAttr->SetAnimValue(sandwichResultValue); + if (NS_FAILED(rv)) { + NS_WARNING("nsISMILAttr::SetAnimValue failed"); + } +} + +void +nsSMILCompositor::ClearAnimationEffects() +{ + if (!mKey.mElement || !mKey.mAttributeName) + return; + + nsAutoPtr<nsISMILAttr> smilAttr(CreateSMILAttr()); + if (!smilAttr) { + // Target attribute not found (or, out of memory) + return; + } + smilAttr->ClearAnimValue(); +} + +// Protected Helper Functions +// -------------------------- +nsISMILAttr* +nsSMILCompositor::CreateSMILAttr() +{ + if (mKey.mIsCSS) { + nsCSSPropertyID propId = + nsCSSProps::LookupProperty(nsDependentAtomString(mKey.mAttributeName), + CSSEnabledState::eForAllContent); + if (nsSMILCSSProperty::IsPropertyAnimatable(propId)) { + return new nsSMILCSSProperty(propId, mKey.mElement.get()); + } + } else { + return mKey.mElement->GetAnimatedAttr(mKey.mAttributeNamespaceID, + mKey.mAttributeName); + } + return nullptr; +} + +uint32_t +nsSMILCompositor::GetFirstFuncToAffectSandwich() +{ + // For performance reasons, we throttle most animations on elements in + // display:none subtrees. (We can't throttle animations that target the + // "display" property itself, though -- if we did, display:none elements + // could never be dynamically displayed via animations.) + // To determine whether we're in a display:none subtree, we will check the + // element's primary frame since element in display:none subtree doesn't have + // a primary frame. Before this process, we will construct frame when we + // append an element to subtree. So we will not need to worry about pending + // frame construction in this step. + bool canThrottle = mKey.mAttributeName != nsGkAtoms::display && + !mKey.mElement->GetPrimaryFrame(); + + uint32_t i; + for (i = mAnimationFunctions.Length(); i > 0; --i) { + nsSMILAnimationFunction* curAnimFunc = mAnimationFunctions[i-1]; + // In the following, the lack of short-circuit behavior of |= means that we + // will ALWAYS run UpdateCachedTarget (even if mForceCompositing is true) + // but only call HasChanged and WasSkippedInPrevSample if necessary. This + // is important since we need UpdateCachedTarget to run in order to detect + // changes to the target in subsequent samples. + mForceCompositing |= + curAnimFunc->UpdateCachedTarget(mKey) || + (curAnimFunc->HasChanged() && !canThrottle) || + curAnimFunc->WasSkippedInPrevSample(); + + if (curAnimFunc->WillReplace()) { + --i; + break; + } + } + + // Mark remaining animation functions as having been skipped so if we later + // use them we'll know to force compositing. + // Note that we only really need to do this if something has changed + // (otherwise we would have set the flag on a previous sample) and if + // something has changed mForceCompositing will be true. + if (mForceCompositing) { + for (uint32_t j = i; j > 0; --j) { + mAnimationFunctions[j-1]->SetWasSkipped(); + } + } + return i; +} + +void +nsSMILCompositor::UpdateCachedBaseValue(const nsSMILValue& aBaseValue) +{ + if (!mCachedBaseValue) { + // We don't have last sample's base value cached. Assume it's changed. + mCachedBaseValue = new nsSMILValue(aBaseValue); + NS_WARNING_ASSERTION(mCachedBaseValue, "failed to cache base value (OOM?)"); + mForceCompositing = true; + } else if (*mCachedBaseValue != aBaseValue) { + // Base value has changed since last sample. + *mCachedBaseValue = aBaseValue; + mForceCompositing = true; + } +} diff --git a/dom/smil/nsSMILCompositor.h b/dom/smil/nsSMILCompositor.h new file mode 100644 index 000000000..ed87ffa8c --- /dev/null +++ b/dom/smil/nsSMILCompositor.h @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILCOMPOSITOR_H_ +#define NS_SMILCOMPOSITOR_H_ + +#include "mozilla/Move.h" +#include "nsAutoPtr.h" +#include "nsTHashtable.h" +#include "nsString.h" +#include "nsSMILAnimationFunction.h" +#include "nsSMILTargetIdentifier.h" +#include "nsSMILCompositorTable.h" +#include "PLDHashTable.h" + +//---------------------------------------------------------------------- +// nsSMILCompositor +// +// Performs the composition of the animation sandwich by combining the results +// of a series animation functions according to the rules of SMIL composition +// including prioritising animations. + +class nsSMILCompositor : public PLDHashEntryHdr +{ +public: + typedef nsSMILTargetIdentifier KeyType; + typedef const KeyType& KeyTypeRef; + typedef const KeyType* KeyTypePointer; + + explicit nsSMILCompositor(KeyTypePointer aKey) + : mKey(*aKey), + mForceCompositing(false) + { } + nsSMILCompositor(const nsSMILCompositor& toCopy) + : mKey(toCopy.mKey), + mAnimationFunctions(toCopy.mAnimationFunctions), + mForceCompositing(false) + { } + ~nsSMILCompositor() { } + + // PLDHashEntryHdr methods + KeyTypeRef GetKey() const { return mKey; } + bool KeyEquals(KeyTypePointer aKey) const; + static KeyTypePointer KeyToPointer(KeyTypeRef aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey); + enum { ALLOW_MEMMOVE = false }; + + // Adds the given animation function to this Compositor's list of functions + void AddAnimationFunction(nsSMILAnimationFunction* aFunc); + + // Composes the attribute's current value with the list of animation + // functions, and assigns the resulting value to this compositor's target + // attribute. If a change is made that might produce style updates, + // aMightHavePendingStyleUpdates is set to true. Otherwise it is not modified. + void ComposeAttribute(bool& aMightHavePendingStyleUpdates); + + // Clears animation effects on my target attribute + void ClearAnimationEffects(); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + + // Toggles a bit that will force us to composite (bypassing early-return + // optimizations) when we hit ComposeAttribute. + void ToggleForceCompositing() { mForceCompositing = true; } + + // Transfers |aOther|'s mCachedBaseValue to |this| + void StealCachedBaseValue(nsSMILCompositor* aOther) { + mCachedBaseValue = mozilla::Move(aOther->mCachedBaseValue); + } + + private: + // Create a nsISMILAttr for my target, on the heap. Caller is responsible + // for deallocating the returned object. + nsISMILAttr* CreateSMILAttr(); + + // Finds the index of the first function that will affect our animation + // sandwich. Also toggles the 'mForceCompositing' flag if it finds that any + // (used) functions have changed. + uint32_t GetFirstFuncToAffectSandwich(); + + // If the passed-in base value differs from our cached base value, this + // method updates the cached value (and toggles the 'mForceCompositing' flag) + void UpdateCachedBaseValue(const nsSMILValue& aBaseValue); + + // The hash key (tuple of element/attributeName/attributeType) + KeyType mKey; + + // Hash Value: List of animation functions that animate the specified attr + nsTArray<nsSMILAnimationFunction*> mAnimationFunctions; + + // Member data for detecting when we need to force-recompose + // --------------------------------------------------------- + // Flag for tracking whether we need to compose. Initialized to false, but + // gets flipped to true if we detect that something has changed. + bool mForceCompositing; + + // Cached base value, so we can detect & force-recompose when it changes + // from one sample to the next. (nsSMILAnimationController copies this + // forward from the previous sample's compositor.) + nsAutoPtr<nsSMILValue> mCachedBaseValue; +}; + +#endif // NS_SMILCOMPOSITOR_H_ diff --git a/dom/smil/nsSMILCompositorTable.h b/dom/smil/nsSMILCompositorTable.h new file mode 100644 index 000000000..b35d50117 --- /dev/null +++ b/dom/smil/nsSMILCompositorTable.h @@ -0,0 +1,23 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILCOMPOSITORTABLE_H_ +#define NS_SMILCOMPOSITORTABLE_H_ + +#include "nsTHashtable.h" + +//---------------------------------------------------------------------- +// nsSMILCompositorTable : A hashmap of nsSMILCompositors +// +// This is just a forward-declaration because it is included in +// nsSMILAnimationController which is used in nsDocument. We don't want to +// expose all of nsSMILCompositor or otherwise any changes to it will mean the +// whole world will need to be rebuilt. + +class nsSMILCompositor; +typedef nsTHashtable<nsSMILCompositor> nsSMILCompositorTable; + +#endif // NS_SMILCOMPOSITORTABLE_H_ diff --git a/dom/smil/nsSMILFloatType.cpp b/dom/smil/nsSMILFloatType.cpp new file mode 100644 index 000000000..d3e298043 --- /dev/null +++ b/dom/smil/nsSMILFloatType.cpp @@ -0,0 +1,92 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILFloatType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" +#include <math.h> + +void +nsSMILFloatType::Init(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mDouble = 0.0; + aValue.mType = this; +} + +void +nsSMILFloatType::Destroy(nsSMILValue& aValue) const +{ + NS_PRECONDITION(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mDouble = 0.0; + aValue.mType = nsSMILNullType::Singleton(); +} + +nsresult +nsSMILFloatType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mDouble = aSrc.mU.mDouble; + return NS_OK; +} + +bool +nsSMILFloatType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mDouble == aRight.mU.mDouble; +} + +nsresult +nsSMILFloatType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_PRECONDITION(aValueToAdd.mType == aDest.mType, + "Trying to add invalid types"); + NS_PRECONDITION(aValueToAdd.mType == this, "Unexpected source type"); + aDest.mU.mDouble += aValueToAdd.mU.mDouble * aCount; + return NS_OK; +} + +nsresult +nsSMILFloatType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_PRECONDITION(aFrom.mType == aTo.mType,"Trying to compare different types"); + NS_PRECONDITION(aFrom.mType == this, "Unexpected source type"); + + const double &from = aFrom.mU.mDouble; + const double &to = aTo.mU.mDouble; + + aDistance = fabs(to - from); + + return NS_OK; +} + +nsresult +nsSMILFloatType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_PRECONDITION(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + NS_PRECONDITION(aStartVal.mType == this, + "Unexpected types for interpolation"); + NS_PRECONDITION(aResult.mType == this, "Unexpected result type"); + + const double &startVal = aStartVal.mU.mDouble; + const double &endVal = aEndVal.mU.mDouble; + + aResult.mU.mDouble = (startVal + (endVal - startVal) * aUnitDistance); + + return NS_OK; +} diff --git a/dom/smil/nsSMILFloatType.h b/dom/smil/nsSMILFloatType.h new file mode 100644 index 000000000..fd57e4a77 --- /dev/null +++ b/dom/smil/nsSMILFloatType.h @@ -0,0 +1,47 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILFLOATTYPE_H_ +#define NS_SMILFLOATTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +class nsSMILFloatType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static nsSMILFloatType* + Singleton() + { + static nsSMILFloatType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override; + virtual void Destroy(nsSMILValue& aValue) const override; + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr nsSMILFloatType() {} +}; + +#endif // NS_SMILFLOATTYPE_H_ diff --git a/dom/smil/nsSMILInstanceTime.cpp b/dom/smil/nsSMILInstanceTime.cpp new file mode 100644 index 000000000..f5d27fdee --- /dev/null +++ b/dom/smil/nsSMILInstanceTime.cpp @@ -0,0 +1,212 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILInstanceTime.h" +#include "nsSMILInterval.h" +#include "nsSMILTimeValueSpec.h" +#include "mozilla/AutoRestore.h" + +//---------------------------------------------------------------------- +// Implementation + +nsSMILInstanceTime::nsSMILInstanceTime(const nsSMILTimeValue& aTime, + nsSMILInstanceTimeSource aSource, + nsSMILTimeValueSpec* aCreator, + nsSMILInterval* aBaseInterval) + : mTime(aTime), + mFlags(0), + mVisited(false), + mFixedEndpointRefCnt(0), + mSerial(0), + mCreator(aCreator), + mBaseInterval(nullptr) // This will get set to aBaseInterval in a call to + // SetBaseInterval() at end of constructor +{ + switch (aSource) { + case SOURCE_NONE: + // No special flags + break; + + case SOURCE_DOM: + mFlags = kDynamic | kFromDOM; + break; + + case SOURCE_SYNCBASE: + mFlags = kMayUpdate; + break; + + case SOURCE_EVENT: + mFlags = kDynamic; + break; + } + + SetBaseInterval(aBaseInterval); +} + +nsSMILInstanceTime::~nsSMILInstanceTime() +{ + MOZ_ASSERT(!mBaseInterval, + "Destroying instance time without first calling Unlink()"); + MOZ_ASSERT(mFixedEndpointRefCnt == 0, + "Destroying instance time that is still used as the fixed " + "endpoint of an interval"); +} + +void +nsSMILInstanceTime::Unlink() +{ + RefPtr<nsSMILInstanceTime> deathGrip(this); + if (mBaseInterval) { + mBaseInterval->RemoveDependentTime(*this); + mBaseInterval = nullptr; + } + mCreator = nullptr; +} + +void +nsSMILInstanceTime::HandleChangedInterval( + const nsSMILTimeContainer* aSrcContainer, + bool aBeginObjectChanged, + bool aEndObjectChanged) +{ + // It's possible a sequence of notifications might cause our base interval to + // be updated and then deleted. Furthermore, the delete might happen whilst + // we're still in the queue to be notified of the change. In any case, if we + // don't have a base interval, just ignore the change. + if (!mBaseInterval) + return; + + MOZ_ASSERT(mCreator, "Base interval is set but creator is not."); + + if (mVisited) { + // Break the cycle here + Unlink(); + return; + } + + bool objectChanged = mCreator->DependsOnBegin() ? aBeginObjectChanged : + aEndObjectChanged; + + RefPtr<nsSMILInstanceTime> deathGrip(this); + mozilla::AutoRestore<bool> setVisited(mVisited); + mVisited = true; + + mCreator->HandleChangedInstanceTime(*GetBaseTime(), aSrcContainer, *this, + objectChanged); +} + +void +nsSMILInstanceTime::HandleDeletedInterval() +{ + MOZ_ASSERT(mBaseInterval, + "Got call to HandleDeletedInterval on an independent instance " + "time"); + MOZ_ASSERT(mCreator, "Base interval is set but creator is not"); + + mBaseInterval = nullptr; + mFlags &= ~kMayUpdate; // Can't update without a base interval + + RefPtr<nsSMILInstanceTime> deathGrip(this); + mCreator->HandleDeletedInstanceTime(*this); + mCreator = nullptr; +} + +void +nsSMILInstanceTime::HandleFilteredInterval() +{ + MOZ_ASSERT(mBaseInterval, + "Got call to HandleFilteredInterval on an independent instance " + "time"); + + mBaseInterval = nullptr; + mFlags &= ~kMayUpdate; // Can't update without a base interval + mCreator = nullptr; +} + +bool +nsSMILInstanceTime::ShouldPreserve() const +{ + return mFixedEndpointRefCnt > 0 || (mFlags & kWasDynamicEndpoint); +} + +void +nsSMILInstanceTime::UnmarkShouldPreserve() +{ + mFlags &= ~kWasDynamicEndpoint; +} + +void +nsSMILInstanceTime::AddRefFixedEndpoint() +{ + MOZ_ASSERT(mFixedEndpointRefCnt < UINT16_MAX, + "Fixed endpoint reference count upper limit reached"); + ++mFixedEndpointRefCnt; + mFlags &= ~kMayUpdate; // Once fixed, always fixed +} + +void +nsSMILInstanceTime::ReleaseFixedEndpoint() +{ + MOZ_ASSERT(mFixedEndpointRefCnt > 0, "Duplicate release"); + --mFixedEndpointRefCnt; + if (mFixedEndpointRefCnt == 0 && IsDynamic()) { + mFlags |= kWasDynamicEndpoint; + } +} + +bool +nsSMILInstanceTime::IsDependentOn(const nsSMILInstanceTime& aOther) const +{ + if (mVisited) + return false; + + const nsSMILInstanceTime* myBaseTime = GetBaseTime(); + if (!myBaseTime) + return false; + + if (myBaseTime == &aOther) + return true; + + mozilla::AutoRestore<bool> setVisited(mVisited); + mVisited = true; + return myBaseTime->IsDependentOn(aOther); +} + +const nsSMILInstanceTime* +nsSMILInstanceTime::GetBaseTime() const +{ + if (!mBaseInterval) { + return nullptr; + } + + MOZ_ASSERT(mCreator, "Base interval is set but there is no creator."); + if (!mCreator) { + return nullptr; + } + + return mCreator->DependsOnBegin() ? mBaseInterval->Begin() : + mBaseInterval->End(); +} + +void +nsSMILInstanceTime::SetBaseInterval(nsSMILInterval* aBaseInterval) +{ + MOZ_ASSERT(!mBaseInterval, + "Attempting to reassociate an instance time with a different " + "interval."); + + if (aBaseInterval) { + MOZ_ASSERT(mCreator, + "Attempting to create a dependent instance time without " + "reference to the creating nsSMILTimeValueSpec object."); + if (!mCreator) + return; + + aBaseInterval->AddDependentTime(*this); + } + + mBaseInterval = aBaseInterval; +} diff --git a/dom/smil/nsSMILInstanceTime.h b/dom/smil/nsSMILInstanceTime.h new file mode 100644 index 000000000..d5a5807e3 --- /dev/null +++ b/dom/smil/nsSMILInstanceTime.h @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILINSTANCETIME_H_ +#define NS_SMILINSTANCETIME_H_ + +#include "nsSMILTimeValue.h" + +class nsSMILInterval; +class nsSMILTimeContainer; +class nsSMILTimeValueSpec; + +//---------------------------------------------------------------------- +// nsSMILInstanceTime +// +// An instant in document simple time that may be used in creating a new +// interval. +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in nsSMILTimeValue.h +// +// These objects are owned by an nsSMILTimedElement but MAY also be referenced +// by: +// +// a) nsSMILIntervals that belong to the same nsSMILTimedElement and which refer +// to the nsSMILInstanceTimes which form the interval endpoints; and/or +// b) nsSMILIntervals that belong to other nsSMILTimedElements but which need to +// update dependent instance times when they change or are deleted. +// E.g. for begin='a.begin', 'a' needs to inform dependent +// nsSMILInstanceTimes if its begin time changes. This notification is +// performed by the nsSMILInterval. + +class nsSMILInstanceTime final +{ +public: + // Instance time source. Times generated by events, syncbase relationships, + // and DOM calls behave differently in some circumstances such as when a timed + // element is reset. + enum nsSMILInstanceTimeSource { + // No particularly significant source, e.g. offset time, 'indefinite' + SOURCE_NONE, + // Generated by a DOM call such as beginElement + SOURCE_DOM, + // Generated by a syncbase relationship + SOURCE_SYNCBASE, + // Generated by an event + SOURCE_EVENT + }; + + explicit nsSMILInstanceTime(const nsSMILTimeValue& aTime, + nsSMILInstanceTimeSource aSource = SOURCE_NONE, + nsSMILTimeValueSpec* aCreator = nullptr, + nsSMILInterval* aBaseInterval = nullptr); + + void Unlink(); + void HandleChangedInterval(const nsSMILTimeContainer* aSrcContainer, + bool aBeginObjectChanged, + bool aEndObjectChanged); + void HandleDeletedInterval(); + void HandleFilteredInterval(); + + const nsSMILTimeValue& Time() const { return mTime; } + const nsSMILTimeValueSpec* GetCreator() const { return mCreator; } + + bool IsDynamic() const { return !!(mFlags & kDynamic); } + bool IsFixedTime() const { return !(mFlags & kMayUpdate); } + bool FromDOM() const { return !!(mFlags & kFromDOM); } + + bool ShouldPreserve() const; + void UnmarkShouldPreserve(); + + void AddRefFixedEndpoint(); + void ReleaseFixedEndpoint(); + + void DependentUpdate(const nsSMILTimeValue& aNewTime) + { + MOZ_ASSERT(!IsFixedTime(), + "Updating an instance time that is not expected to be updated"); + mTime = aNewTime; + } + + bool IsDependent() const { return !!mBaseInterval; } + bool IsDependentOn(const nsSMILInstanceTime& aOther) const; + const nsSMILInterval* GetBaseInterval() const { return mBaseInterval; } + const nsSMILInstanceTime* GetBaseTime() const; + + bool SameTimeAndBase(const nsSMILInstanceTime& aOther) const + { + return mTime == aOther.mTime && GetBaseTime() == aOther.GetBaseTime(); + } + + // Get and set a serial number which may be used by a containing class to + // control the sort order of otherwise similar instance times. + uint32_t Serial() const { return mSerial; } + void SetSerial(uint32_t aIndex) { mSerial = aIndex; } + + NS_INLINE_DECL_REFCOUNTING(nsSMILInstanceTime) + +private: + // Private destructor, to discourage deletion outside of Release(): + ~nsSMILInstanceTime(); + + void SetBaseInterval(nsSMILInterval* aBaseInterval); + + nsSMILTimeValue mTime; + + // Internal flags used to represent the behaviour of different instance times + enum { + // Indicates that this instance time was generated by an event or a DOM + // call. Such instance times require special handling when (i) the owning + // element is reset, (ii) when they are to be added as a new end instance + // times (as per SMIL's event sensitivity contraints), and (iii) when + // a backwards seek is performed and the timing model is reconstructed. + kDynamic = 1, + + // Indicates that this instance time is referred to by an + // nsSMILTimeValueSpec and as such may be updated. Such instance time should + // not be filtered out by the nsSMILTimedElement even if they appear to be + // in the past as they may be updated to a future time. + kMayUpdate = 2, + + // Indicates that this instance time was generated from the DOM as opposed + // to an nsSMILTimeValueSpec. When a 'begin' or 'end' attribute is set or + // reset we should clear all the instance times that have been generated by + // that attribute (and hence an nsSMILTimeValueSpec), but not those from the + // DOM. + kFromDOM = 4, + + // Indicates that this instance time was used as the endpoint of an interval + // that has been filtered or removed. However, since it is a dynamic time it + // should be preserved and not filtered. + kWasDynamicEndpoint = 8 + }; + uint8_t mFlags; // Combination of kDynamic, kMayUpdate, etc. + mutable bool mVisited; // Cycle tracking + + // Additional reference count to determine if this instance time is currently + // used as a fixed endpoint in any intervals. Instance times that are used in + // this way should not be removed when the owning nsSMILTimedElement removes + // instance times in response to a restart or in an attempt to free up memory + // by filtering out old instance times. + // + // Instance times are only shared in a few cases, namely: + // a) early ends, + // b) zero-duration intervals, + // c) momentarily whilst establishing new intervals and updating the current + // interval, and + // d) trimmed intervals + // Hence the limited range of a uint16_t should be more than adequate. + uint16_t mFixedEndpointRefCnt; + + uint32_t mSerial; // A serial number used by the containing class to + // specify the sort order for instance times with the + // same mTime. + + nsSMILTimeValueSpec* mCreator; // The nsSMILTimeValueSpec object that created + // us. (currently only needed for syncbase + // instance times.) + nsSMILInterval* mBaseInterval; // Interval from which this time is derived + // (only used for syncbase instance times) +}; + +#endif // NS_SMILINSTANCETIME_H_ diff --git a/dom/smil/nsSMILInterval.cpp b/dom/smil/nsSMILInterval.cpp new file mode 100644 index 000000000..956efd626 --- /dev/null +++ b/dom/smil/nsSMILInterval.cpp @@ -0,0 +1,170 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILInterval.h" + +nsSMILInterval::nsSMILInterval() +: + mBeginFixed(false), + mEndFixed(false) +{ +} + +nsSMILInterval::nsSMILInterval(const nsSMILInterval& aOther) +: + mBegin(aOther.mBegin), + mEnd(aOther.mEnd), + mBeginFixed(false), + mEndFixed(false) +{ + MOZ_ASSERT(aOther.mDependentTimes.IsEmpty(), + "Attempt to copy-construct an interval with dependent times; this " + "will lead to instance times being shared between intervals."); + + // For the time being we don't allow intervals with fixed endpoints to be + // copied since we only ever copy-construct to establish a new current + // interval. If we ever need to copy historical intervals we may need to move + // the ReleaseFixedEndpoint calls from Unlink to the dtor. + MOZ_ASSERT(!aOther.mBeginFixed && !aOther.mEndFixed, + "Attempt to copy-construct an interval with fixed endpoints"); +} + +nsSMILInterval::~nsSMILInterval() +{ + MOZ_ASSERT(mDependentTimes.IsEmpty(), + "Destroying interval without disassociating dependent instance " + "times. Unlink was not called"); +} + +void +nsSMILInterval::Unlink(bool aFiltered) +{ + for (int32_t i = mDependentTimes.Length() - 1; i >= 0; --i) { + if (aFiltered) { + mDependentTimes[i]->HandleFilteredInterval(); + } else { + mDependentTimes[i]->HandleDeletedInterval(); + } + } + mDependentTimes.Clear(); + if (mBegin && mBeginFixed) { + mBegin->ReleaseFixedEndpoint(); + } + mBegin = nullptr; + if (mEnd && mEndFixed) { + mEnd->ReleaseFixedEndpoint(); + } + mEnd = nullptr; +} + +nsSMILInstanceTime* +nsSMILInterval::Begin() +{ + MOZ_ASSERT(mBegin && mEnd, + "Requesting Begin() on un-initialized interval."); + return mBegin; +} + +nsSMILInstanceTime* +nsSMILInterval::End() +{ + MOZ_ASSERT(mBegin && mEnd, + "Requesting End() on un-initialized interval."); + return mEnd; +} + +void +nsSMILInterval::SetBegin(nsSMILInstanceTime& aBegin) +{ + MOZ_ASSERT(aBegin.Time().IsDefinite(), + "Attempt to set unresolved or indefinite begin time on interval"); + MOZ_ASSERT(!mBeginFixed, + "Attempt to set begin time but the begin point is fixed"); + // Check that we're not making an instance time dependent on itself. Such an + // arrangement does not make intuitive sense and should be detected when + // creating or updating intervals. + MOZ_ASSERT(!mBegin || aBegin.GetBaseTime() != mBegin, + "Attempt to make self-dependent instance time"); + + mBegin = &aBegin; +} + +void +nsSMILInterval::SetEnd(nsSMILInstanceTime& aEnd) +{ + MOZ_ASSERT(!mEndFixed, + "Attempt to set end time but the end point is fixed"); + // As with SetBegin, check we're not making an instance time dependent on + // itself. + MOZ_ASSERT(!mEnd || aEnd.GetBaseTime() != mEnd, + "Attempting to make self-dependent instance time"); + + mEnd = &aEnd; +} + +void +nsSMILInterval::FixBegin() +{ + MOZ_ASSERT(mBegin && mEnd, + "Fixing begin point on un-initialized interval"); + MOZ_ASSERT(!mBeginFixed, "Duplicate calls to FixBegin()"); + mBeginFixed = true; + mBegin->AddRefFixedEndpoint(); +} + +void +nsSMILInterval::FixEnd() +{ + MOZ_ASSERT(mBegin && mEnd, + "Fixing end point on un-initialized interval"); + MOZ_ASSERT(mBeginFixed, + "Fixing the end of an interval without a fixed begin"); + MOZ_ASSERT(!mEndFixed, "Duplicate calls to FixEnd()"); + mEndFixed = true; + mEnd->AddRefFixedEndpoint(); +} + +void +nsSMILInterval::AddDependentTime(nsSMILInstanceTime& aTime) +{ + RefPtr<nsSMILInstanceTime>* inserted = + mDependentTimes.InsertElementSorted(&aTime); + if (!inserted) { + NS_WARNING("Insufficient memory to insert instance time."); + } +} + +void +nsSMILInterval::RemoveDependentTime(const nsSMILInstanceTime& aTime) +{ +#ifdef DEBUG + bool found = +#endif + mDependentTimes.RemoveElementSorted(&aTime); + MOZ_ASSERT(found, "Couldn't find instance time to delete."); +} + +void +nsSMILInterval::GetDependentTimes(InstanceTimeList& aTimes) +{ + aTimes = mDependentTimes; +} + +bool +nsSMILInterval::IsDependencyChainLink() const +{ + if (!mBegin || !mEnd) + return false; // Not yet initialised so it can't be part of a chain + + if (mDependentTimes.IsEmpty()) + return false; // No dependents, chain end + + // So we have dependents, but we're still only a link in the chain (as opposed + // to the end of the chain) if one of our endpoints is dependent on an + // interval other than ourselves. + return (mBegin->IsDependent() && mBegin->GetBaseInterval() != this) || + (mEnd->IsDependent() && mEnd->GetBaseInterval() != this); +} diff --git a/dom/smil/nsSMILInterval.h b/dom/smil/nsSMILInterval.h new file mode 100644 index 000000000..d30728821 --- /dev/null +++ b/dom/smil/nsSMILInterval.h @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILINTERVAL_H_ +#define NS_SMILINTERVAL_H_ + +#include "nsSMILInstanceTime.h" +#include "nsTArray.h" + +//---------------------------------------------------------------------- +// nsSMILInterval class +// +// A structure consisting of a begin and end time. The begin time must be +// resolved (i.e. not indefinite or unresolved). +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in nsSMILTimeValue.h + +class nsSMILInterval +{ +public: + nsSMILInterval(); + nsSMILInterval(const nsSMILInterval& aOther); + ~nsSMILInterval(); + void Unlink(bool aFiltered = false); + + const nsSMILInstanceTime* Begin() const + { + MOZ_ASSERT(mBegin && mEnd, + "Requesting Begin() on un-initialized instance time"); + return mBegin; + } + nsSMILInstanceTime* Begin(); + + const nsSMILInstanceTime* End() const + { + MOZ_ASSERT(mBegin && mEnd, + "Requesting End() on un-initialized instance time"); + return mEnd; + } + nsSMILInstanceTime* End(); + + void SetBegin(nsSMILInstanceTime& aBegin); + void SetEnd(nsSMILInstanceTime& aEnd); + void Set(nsSMILInstanceTime& aBegin, nsSMILInstanceTime& aEnd) + { + SetBegin(aBegin); + SetEnd(aEnd); + } + + void FixBegin(); + void FixEnd(); + + typedef nsTArray<RefPtr<nsSMILInstanceTime> > InstanceTimeList; + + void AddDependentTime(nsSMILInstanceTime& aTime); + void RemoveDependentTime(const nsSMILInstanceTime& aTime); + void GetDependentTimes(InstanceTimeList& aTimes); + + // Cue for assessing if this interval can be filtered + bool IsDependencyChainLink() const; + +private: + RefPtr<nsSMILInstanceTime> mBegin; + RefPtr<nsSMILInstanceTime> mEnd; + + // nsSMILInstanceTimes to notify when this interval is changed or deleted. + InstanceTimeList mDependentTimes; + + // Indicates if the end points of the interval are fixed or not. + // + // Note that this is not the same as having an end point whose TIME is fixed + // (i.e. nsSMILInstanceTime::IsFixed() returns true). This is because it is + // possible to have an end point with a fixed TIME and yet still update the + // end point to refer to a different nsSMILInstanceTime object. + // + // However, if mBegin/EndFixed is true, then BOTH the nsSMILInstanceTime + // OBJECT returned for that end point and its TIME value will not change. + bool mBeginFixed; + bool mEndFixed; +}; + +#endif // NS_SMILINTERVAL_H_ diff --git a/dom/smil/nsSMILKeySpline.cpp b/dom/smil/nsSMILKeySpline.cpp new file mode 100644 index 000000000..716437aab --- /dev/null +++ b/dom/smil/nsSMILKeySpline.cpp @@ -0,0 +1,151 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILKeySpline.h" +#include <stdint.h> +#include <math.h> + +#define NEWTON_ITERATIONS 4 +#define NEWTON_MIN_SLOPE 0.02 +#define SUBDIVISION_PRECISION 0.0000001 +#define SUBDIVISION_MAX_ITERATIONS 10 + +const double nsSMILKeySpline::kSampleStepSize = + 1.0 / double(kSplineTableSize - 1); + +void +nsSMILKeySpline::Init(double aX1, + double aY1, + double aX2, + double aY2) +{ + mX1 = aX1; + mY1 = aY1; + mX2 = aX2; + mY2 = aY2; + + if (mX1 != mY1 || mX2 != mY2) + CalcSampleValues(); +} + +double +nsSMILKeySpline::GetSplineValue(double aX) const +{ + if (mX1 == mY1 && mX2 == mY2) + return aX; + + return CalcBezier(GetTForX(aX), mY1, mY2); +} + +void +nsSMILKeySpline::GetSplineDerivativeValues(double aX, double& aDX, double& aDY) const +{ + double t = GetTForX(aX); + aDX = GetSlope(t, mX1, mX2); + aDY = GetSlope(t, mY1, mY2); +} + +void +nsSMILKeySpline::CalcSampleValues() +{ + for (uint32_t i = 0; i < kSplineTableSize; ++i) { + mSampleValues[i] = CalcBezier(double(i) * kSampleStepSize, mX1, mX2); + } +} + +/*static*/ double +nsSMILKeySpline::CalcBezier(double aT, + double aA1, + double aA2) +{ + // use Horner's scheme to evaluate the Bezier polynomial + return ((A(aA1, aA2)*aT + B(aA1, aA2))*aT + C(aA1))*aT; +} + +/*static*/ double +nsSMILKeySpline::GetSlope(double aT, + double aA1, + double aA2) +{ + return 3.0 * A(aA1, aA2)*aT*aT + 2.0 * B(aA1, aA2) * aT + C(aA1); +} + +double +nsSMILKeySpline::GetTForX(double aX) const +{ + // Early return when aX == 1.0 to avoid floating-point inaccuracies. + if (aX == 1.0) { + return 1.0; + } + // Find interval where t lies + double intervalStart = 0.0; + const double* currentSample = &mSampleValues[1]; + const double* const lastSample = &mSampleValues[kSplineTableSize - 1]; + for (; currentSample != lastSample && *currentSample <= aX; + ++currentSample) { + intervalStart += kSampleStepSize; + } + --currentSample; // t now lies between *currentSample and *currentSample+1 + + // Interpolate to provide an initial guess for t + double dist = (aX - *currentSample) / + (*(currentSample+1) - *currentSample); + double guessForT = intervalStart + dist * kSampleStepSize; + + // Check the slope to see what strategy to use. If the slope is too small + // Newton-Raphson iteration won't converge on a root so we use bisection + // instead. + double initialSlope = GetSlope(guessForT, mX1, mX2); + if (initialSlope >= NEWTON_MIN_SLOPE) { + return NewtonRaphsonIterate(aX, guessForT); + } else if (initialSlope == 0.0) { + return guessForT; + } else { + return BinarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize); + } +} + +double +nsSMILKeySpline::NewtonRaphsonIterate(double aX, double aGuessT) const +{ + // Refine guess with Newton-Raphson iteration + for (uint32_t i = 0; i < NEWTON_ITERATIONS; ++i) { + // We're trying to find where f(t) = aX, + // so we're actually looking for a root for: CalcBezier(t) - aX + double currentX = CalcBezier(aGuessT, mX1, mX2) - aX; + double currentSlope = GetSlope(aGuessT, mX1, mX2); + + if (currentSlope == 0.0) + return aGuessT; + + aGuessT -= currentX / currentSlope; + } + + return aGuessT; +} + +double +nsSMILKeySpline::BinarySubdivide(double aX, double aA, double aB) const +{ + double currentX; + double currentT; + uint32_t i = 0; + + do + { + currentT = aA + (aB - aA) / 2.0; + currentX = CalcBezier(currentT, mX1, mX2) - aX; + + if (currentX > 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (fabs(currentX) > SUBDIVISION_PRECISION + && ++i < SUBDIVISION_MAX_ITERATIONS); + + return currentT; +} diff --git a/dom/smil/nsSMILKeySpline.h b/dom/smil/nsSMILKeySpline.h new file mode 100644 index 000000000..36c14fec1 --- /dev/null +++ b/dom/smil/nsSMILKeySpline.h @@ -0,0 +1,122 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILKEYSPLINE_H_ +#define NS_SMILKEYSPLINE_H_ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/PodOperations.h" + +/** + * Utility class to provide scaling defined in a keySplines element. + */ +class nsSMILKeySpline +{ +public: + nsSMILKeySpline() { /* caller must call Init later */ } + + /** + * Creates a new key spline control point description. + * + * aX1, etc. are the x1, y1, x2, y2 cubic Bezier control points as defined by + * SMILANIM 3.2.3. They must each be in the range 0.0 <= x <= 1.0 + */ + nsSMILKeySpline(double aX1, double aY1, + double aX2, double aY2) + { + Init(aX1, aY1, aX2, aY2); + } + + double X1() const { return mX1; } + double Y1() const { return mY1; } + double X2() const { return mX2; } + double Y2() const { return mY2; } + + void Init(double aX1, double aY1, + double aX2, double aY2); + + /** + * Gets the output (y) value for an input (x). + * + * @param aX The input x value. A floating-point number between 0 and + * 1 (inclusive). + */ + double GetSplineValue(double aX) const; + + void GetSplineDerivativeValues(double aX, double& aDX, double& aDY) const; + + bool operator==(const nsSMILKeySpline& aOther) const { + return mX1 == aOther.mX1 && + mY1 == aOther.mY1 && + mX2 == aOther.mX2 && + mY2 == aOther.mY2; + } + bool operator!=(const nsSMILKeySpline& aOther) const { + return !(*this == aOther); + } + int32_t Compare(const nsSMILKeySpline& aRhs) const { + if (mX1 != aRhs.mX1) return mX1 < aRhs.mX1 ? -1 : 1; + if (mY1 != aRhs.mY1) return mY1 < aRhs.mY1 ? -1 : 1; + if (mX2 != aRhs.mX2) return mX2 < aRhs.mX2 ? -1 : 1; + if (mY2 != aRhs.mY2) return mY2 < aRhs.mY2 ? -1 : 1; + return 0; + } + +private: + void + CalcSampleValues(); + + /** + * Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. + */ + static double + CalcBezier(double aT, double aA1, double aA2); + + /** + * Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. + */ + static double + GetSlope(double aT, double aA1, double aA2); + + double + GetTForX(double aX) const; + + double + NewtonRaphsonIterate(double aX, double aGuessT) const; + + double + BinarySubdivide(double aX, double aA, double aB) const; + + static double + A(double aA1, double aA2) + { + return 1.0 - 3.0 * aA2 + 3.0 * aA1; + } + + static double + B(double aA1, double aA2) + { + return 3.0 * aA2 - 6.0 * aA1; + } + + static double + C(double aA1) + { + return 3.0 * aA1; + } + + double mX1; + double mY1; + double mX2; + double mY2; + + enum { kSplineTableSize = 11 }; + double mSampleValues[kSplineTableSize]; + + static const double kSampleStepSize; +}; + +#endif // NS_SMILKEYSPLINE_H_ diff --git a/dom/smil/nsSMILMappedAttribute.cpp b/dom/smil/nsSMILMappedAttribute.cpp new file mode 100644 index 000000000..b43469fac --- /dev/null +++ b/dom/smil/nsSMILMappedAttribute.cpp @@ -0,0 +1,150 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a SMIL-animatable mapped attribute on an element */ +#include "nsSMILMappedAttribute.h" +#include "nsContentUtils.h" +#include "nsError.h" // For NS_PROPTABLE_PROP_OVERWRITTEN +#include "nsSMILValue.h" +#include "nsSMILCSSValueType.h" +#include "nsIDocument.h" +#include "nsIPresShell.h" +#include "nsCSSProps.h" +#include "mozilla/dom/Element.h" + +// Callback function, for freeing string buffers stored in property table +static void +ReleaseStringBufferPropertyValue(void* aObject, /* unused */ + nsIAtom* aPropertyName, /* unused */ + void* aPropertyValue, + void* aData /* unused */) +{ + nsStringBuffer* buf = static_cast<nsStringBuffer*>(aPropertyValue); + buf->Release(); +} + + +nsresult +nsSMILMappedAttribute::ValueFromString(const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const +{ + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + nsSMILCSSValueType::ValueFromString(mPropID, mElement, aStr, aValue, + &aPreventCachingOfSandwich); + return aValue.IsNull() ? NS_ERROR_FAILURE : NS_OK; +} + +nsSMILValue +nsSMILMappedAttribute::GetBaseValue() const +{ + nsAutoString baseStringValue; + RefPtr<nsIAtom> attrName = GetAttrNameAtom(); + bool success = mElement->GetAttr(kNameSpaceID_None, attrName, + baseStringValue); + nsSMILValue baseValue; + if (success) { + // For base values, we don't need to worry whether the value returned is + // context-sensitive or not since the compositor will take care of comparing + // the returned (computed) base value and its cached value and determining + // if an update is required or not. + nsSMILCSSValueType::ValueFromString(mPropID, mElement, + baseStringValue, baseValue, nullptr); + } else { + // Attribute is unset -- use computed value. + // FIRST: Temporarily clear animated value, to make sure it doesn't pollute + // the computed value. (We want base value, _without_ animations applied.) + void* buf = mElement->UnsetProperty(SMIL_MAPPED_ATTR_ANIMVAL, + attrName, nullptr); + FlushChangesToTargetAttr(); + + // SECOND: we use nsSMILCSSProperty::GetBaseValue to look up the property's + // computed value. NOTE: This call will temporarily clear the SMIL + // override-style for the corresponding CSS property on our target element. + // This prevents any animations that target the CSS property from affecting + // animations that target the mapped attribute. + baseValue = nsSMILCSSProperty::GetBaseValue(); + + // FINALLY: If we originally had an animated value set, then set it again. + if (buf) { + mElement->SetProperty(SMIL_MAPPED_ATTR_ANIMVAL, attrName, buf, + ReleaseStringBufferPropertyValue); + FlushChangesToTargetAttr(); + } + } + return baseValue; +} + +nsresult +nsSMILMappedAttribute::SetAnimValue(const nsSMILValue& aValue) +{ + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + // Convert nsSMILValue to string + nsAutoString valStr; + if (!nsSMILCSSValueType::ValueToString(aValue, valStr)) { + NS_WARNING("Failed to convert nsSMILValue for mapped attr into a string"); + return NS_ERROR_FAILURE; + } + + RefPtr<nsIAtom> attrName = GetAttrNameAtom(); + nsStringBuffer* oldValStrBuf = static_cast<nsStringBuffer*> + (mElement->GetProperty(SMIL_MAPPED_ATTR_ANIMVAL, attrName)); + if (oldValStrBuf) { + nsString oldValStr; + nsContentUtils::PopulateStringFromStringBuffer(oldValStrBuf, oldValStr); + if (valStr.Equals(oldValStr)) { + // New animated value is the same as the old; nothing to do. + return NS_OK; + } + } + + // Set the string as this mapped attribute's animated value. + nsStringBuffer* valStrBuf = + nsCSSValue::BufferFromString(nsString(valStr)).take(); + nsresult rv = mElement->SetProperty(SMIL_MAPPED_ATTR_ANIMVAL, + attrName, valStrBuf, + ReleaseStringBufferPropertyValue); + if (rv == NS_PROPTABLE_PROP_OVERWRITTEN) { + rv = NS_OK; + } + FlushChangesToTargetAttr(); + + return rv; +} + +void +nsSMILMappedAttribute::ClearAnimValue() +{ + RefPtr<nsIAtom> attrName = GetAttrNameAtom(); + mElement->DeleteProperty(SMIL_MAPPED_ATTR_ANIMVAL, attrName); + FlushChangesToTargetAttr(); +} + +void +nsSMILMappedAttribute::FlushChangesToTargetAttr() const +{ + // Clear animated content-style-rule + mElement->DeleteProperty(SMIL_MAPPED_ATTR_ANIMVAL, + SMIL_MAPPED_ATTR_STYLERULE_ATOM); + nsIDocument* doc = mElement->GetUncomposedDoc(); + + // Request animation restyle + if (doc) { + nsIPresShell* shell = doc->GetShell(); + if (shell) { + shell->RestyleForAnimation(mElement, eRestyle_Self); + } + } +} + +already_AddRefed<nsIAtom> +nsSMILMappedAttribute::GetAttrNameAtom() const +{ + return NS_Atomize(nsCSSProps::GetStringValue(mPropID)); +} diff --git a/dom/smil/nsSMILMappedAttribute.h b/dom/smil/nsSMILMappedAttribute.h new file mode 100644 index 000000000..212b65c92 --- /dev/null +++ b/dom/smil/nsSMILMappedAttribute.h @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a SMIL-animatable mapped attribute on an element */ + +#ifndef NS_SMILMAPPEDATTRIBUTE_H_ +#define NS_SMILMAPPEDATTRIBUTE_H_ + +#include "mozilla/Attributes.h" +#include "nsSMILCSSProperty.h" + +/* We'll use the empty-string atom |nsGkAtoms::_empty| as the key for storing + * an element's animated content style rule in its Property Table, under the + * property-category SMIL_MAPPED_ATTR_ANIMVAL. Everything else stored in that + * category is keyed off of the XML attribute name, so the empty string is a + * good "reserved" key to use for storing the style rule (since XML attributes + * all have nonempty names). + */ +#define SMIL_MAPPED_ATTR_STYLERULE_ATOM nsGkAtoms::_empty + +/** + * nsSMILMappedAttribute: Implements the nsISMILAttr interface for SMIL + * animations whose targets are attributes that map to CSS properties. An + * instance of this class represents a particular animation-targeted mapped + * attribute on a particular element. + */ +class nsSMILMappedAttribute : public nsSMILCSSProperty { +public: + /** + * Constructs a new nsSMILMappedAttribute. + * + * @param aPropID The CSS property for the mapped attribute we're + * interested in animating. + * @param aElement The element whose attribute is being animated. + */ + nsSMILMappedAttribute(nsCSSPropertyID aPropID, mozilla::dom::Element* aElement) : + nsSMILCSSProperty(aPropID, aElement) {} + + // nsISMILAttr methods + virtual nsresult ValueFromString(const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, + nsSMILValue& aValue, + bool& aPreventCachingOfSandwich) const override; + virtual nsSMILValue GetBaseValue() const override; + virtual nsresult SetAnimValue(const nsSMILValue& aValue) override; + virtual void ClearAnimValue() override; + +protected: + // Helper Methods + void FlushChangesToTargetAttr() const; + already_AddRefed<nsIAtom> GetAttrNameAtom() const; +}; +#endif // NS_SMILMAPPEDATTRIBUTE_H_ diff --git a/dom/smil/nsSMILMilestone.h b/dom/smil/nsSMILMilestone.h new file mode 100644 index 000000000..e5f330114 --- /dev/null +++ b/dom/smil/nsSMILMilestone.h @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILMILESTONE_H_ +#define NS_SMILMILESTONE_H_ + +/* + * A significant moment in an nsSMILTimedElement's lifetime where a sample is + * required. + * + * Animations register the next milestone in their lifetime with the time + * container that they belong to. When the animation controller goes to run + * a sample it first visits all the animations that have a registered milestone + * in order of their milestone times. This allows interdependencies between + * animations to be correctly resolved and events to fire in the proper order. + * + * A distinction is made between a milestone representing the end of an interval + * and any other milestone. This is because if animation A ends at time t, and + * animation B begins at the same time t (or has some other significant moment + * such as firing a repeat event), SMIL's endpoint-exclusive timing model + * implies that the interval end occurs first. In fact, interval ends can be + * thought of as ending an infinitesimally small time before t. Therefore, + * A should be sampled before B. + * + * Furthermore, this distinction between sampling the end of an interval and + * a regular sample is used within the timing model (specifically in + * nsSMILTimedElement) to ensure that all intervals ending at time t are sampled + * before any new intervals are entered so that we have a fully up-to-date set + * of instance times available before committing to a new interval. Once an + * interval is entered, the begin time is fixed. + */ +class nsSMILMilestone +{ +public: + nsSMILMilestone(nsSMILTime aTime, bool aIsEnd) + : mTime(aTime), mIsEnd(aIsEnd) + { } + + nsSMILMilestone() + : mTime(0), mIsEnd(false) + { } + + bool operator==(const nsSMILMilestone& aOther) const + { + return mTime == aOther.mTime && mIsEnd == aOther.mIsEnd; + } + + bool operator!=(const nsSMILMilestone& aOther) const + { + return !(*this == aOther); + } + + bool operator<(const nsSMILMilestone& aOther) const + { + // Earlier times sort first, and for equal times end milestones sort first + return mTime < aOther.mTime || + (mTime == aOther.mTime && mIsEnd && !aOther.mIsEnd); + } + + bool operator<=(const nsSMILMilestone& aOther) const + { + return *this == aOther || *this < aOther; + } + + bool operator>=(const nsSMILMilestone& aOther) const + { + return !(*this < aOther); + } + + nsSMILTime mTime; // The milestone time. This may be in container time or + // parent container time depending on where it is used. + bool mIsEnd; // true if this milestone corresponds to an interval + // end, false otherwise. +}; + +#endif // NS_SMILMILESTONE_H_ diff --git a/dom/smil/nsSMILNullType.cpp b/dom/smil/nsSMILNullType.cpp new file mode 100644 index 000000000..b51c135af --- /dev/null +++ b/dom/smil/nsSMILNullType.cpp @@ -0,0 +1,56 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILNullType.h" +#include "nsSMILValue.h" +#include "nsDebug.h" + +nsresult +nsSMILNullType::Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const +{ + NS_PRECONDITION(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aSrc.mType == this, "Unexpected source type"); + aDest.mU = aSrc.mU; + aDest.mType = Singleton(); + return NS_OK; +} + +bool +nsSMILNullType::IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const +{ + NS_PRECONDITION(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + NS_PRECONDITION(aLeft.mType == this, "Unexpected type for SMIL value"); + + return true; // All null-typed values are equivalent. +} + +nsresult +nsSMILNullType::Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const +{ + NS_NOTREACHED("Adding NULL type"); + return NS_ERROR_FAILURE; +} + +nsresult +nsSMILNullType::ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const +{ + NS_NOTREACHED("Computing distance for NULL type"); + return NS_ERROR_FAILURE; +} + +nsresult +nsSMILNullType::Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + NS_NOTREACHED("Interpolating NULL type"); + return NS_ERROR_FAILURE; +} diff --git a/dom/smil/nsSMILNullType.h b/dom/smil/nsSMILNullType.h new file mode 100644 index 000000000..d21610ff4 --- /dev/null +++ b/dom/smil/nsSMILNullType.h @@ -0,0 +1,50 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILNULLTYPE_H_ +#define NS_SMILNULLTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nsISMILType.h" + +class nsSMILNullType : public nsISMILType +{ +public: + // Singleton for nsSMILValue objects to hold onto. + static nsSMILNullType* + Singleton() + { + static nsSMILNullType sSingleton; + return &sSingleton; + } + +protected: + // nsISMILType Methods + // ------------------- + virtual void Init(nsSMILValue& aValue) const override {} + virtual void Destroy(nsSMILValue& aValue) const override {} + virtual nsresult Assign(nsSMILValue& aDest, const nsSMILValue& aSrc) const override; + + // The remaining methods should never be called, so although they're very + // simple they don't need to be inline. + virtual bool IsEqual(const nsSMILValue& aLeft, + const nsSMILValue& aRight) const override; + virtual nsresult Add(nsSMILValue& aDest, const nsSMILValue& aValueToAdd, + uint32_t aCount) const override; + virtual nsresult ComputeDistance(const nsSMILValue& aFrom, + const nsSMILValue& aTo, + double& aDistance) const override; + virtual nsresult Interpolate(const nsSMILValue& aStartVal, + const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const override; + +private: + // Private constructor: prevent instances beyond my singleton. + constexpr nsSMILNullType() {} +}; + +#endif // NS_SMILNULLTYPE_H_ diff --git a/dom/smil/nsSMILParserUtils.cpp b/dom/smil/nsSMILParserUtils.cpp new file mode 100644 index 000000000..9174bdd4a --- /dev/null +++ b/dom/smil/nsSMILParserUtils.cpp @@ -0,0 +1,730 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILParserUtils.h" +#include "nsSMILKeySpline.h" +#include "nsISMILAttr.h" +#include "nsSMILValue.h" +#include "nsSMILTimeValue.h" +#include "nsSMILTimeValueSpecParams.h" +#include "nsSMILTypes.h" +#include "nsSMILRepeatCount.h" +#include "nsContentUtils.h" +#include "nsCharSeparatedTokenizer.h" +#include "SVGContentUtils.h" + +using namespace mozilla; +using namespace mozilla::dom; +//------------------------------------------------------------------------------ +// Helper functions and Constants + +namespace { + +const uint32_t MSEC_PER_SEC = 1000; +const uint32_t MSEC_PER_MIN = 1000 * 60; +const uint32_t MSEC_PER_HOUR = 1000 * 60 * 60; + +#define ACCESSKEY_PREFIX_LC NS_LITERAL_STRING("accesskey(") // SMIL2+ +#define ACCESSKEY_PREFIX_CC NS_LITERAL_STRING("accessKey(") // SVG/SMIL ANIM +#define REPEAT_PREFIX NS_LITERAL_STRING("repeat(") +#define WALLCLOCK_PREFIX NS_LITERAL_STRING("wallclock(") + +inline bool +SkipWhitespace(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd) +{ + while (aIter != aEnd) { + if (!IsSVGWhitespace(*aIter)) { + return true; + } + ++aIter; + } + return false; +} + +inline bool +ParseColon(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd) +{ + if (aIter == aEnd || *aIter != ':') { + return false; + } + ++aIter; + return true; +} + +/* + * Exactly two digits in the range 00 - 59 are expected. + */ +bool +ParseSecondsOrMinutes(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + uint32_t& aValue) +{ + if (aIter == aEnd || !SVGContentUtils::IsDigit(*aIter)) { + return false; + } + + RangedPtr<const char16_t> iter(aIter); + + if (++iter == aEnd || !SVGContentUtils::IsDigit(*iter)) { + return false; + } + + uint32_t value = 10 * SVGContentUtils::DecimalDigitValue(*aIter) + + SVGContentUtils::DecimalDigitValue(*iter); + if (value > 59) { + return false; + } + if (++iter != aEnd && SVGContentUtils::IsDigit(*iter)) { + return false; + } + + aValue = value; + aIter = iter; + return true; +} + +inline bool +ParseClockMetric(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + uint32_t& aMultiplier) +{ + if (aIter == aEnd) { + aMultiplier = MSEC_PER_SEC; + return true; + } + + switch (*aIter) { + case 'h': + if (++aIter == aEnd) { + aMultiplier = MSEC_PER_HOUR; + return true; + } + return false; + case 'm': + { + const nsAString& metric = Substring(aIter.get(), aEnd.get()); + if (metric.EqualsLiteral("min")) { + aMultiplier = MSEC_PER_MIN; + aIter = aEnd; + return true; + } + if (metric.EqualsLiteral("ms")) { + aMultiplier = 1; + aIter = aEnd; + return true; + } + } + return false; + case 's': + if (++aIter == aEnd) { + aMultiplier = MSEC_PER_SEC; + return true; + } + } + return false; +} + +/** + * See http://www.w3.org/TR/SVG/animate.html#ClockValueSyntax + */ +bool +ParseClockValue(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + nsSMILTimeValue* aResult) +{ + if (aIter == aEnd) { + return false; + } + + // TIMECOUNT_VALUE ::= Timecount ("." Fraction)? (Metric)? + // PARTIAL_CLOCK_VALUE ::= Minutes ":" Seconds ("." Fraction)? + // FULL_CLOCK_VALUE ::= Hours ":" Minutes ":" Seconds ("." Fraction)? + enum ClockType { + TIMECOUNT_VALUE, + PARTIAL_CLOCK_VALUE, + FULL_CLOCK_VALUE + }; + + int32_t clockType = TIMECOUNT_VALUE; + + RangedPtr<const char16_t> iter(aIter); + + // Determine which type of clock value we have by counting the number + // of colons in the string. + do { + switch (*iter) { + case ':': + if (clockType == FULL_CLOCK_VALUE) { + return false; + } + ++clockType; + break; + case 'e': + case 'E': + case '-': + case '+': + // Exclude anything invalid (for clock values) + // that number parsing might otherwise allow. + return false; + } + ++iter; + } while (iter != aEnd); + + iter = aIter; + + int32_t hours = 0, timecount; + double fraction = 0.0; + uint32_t minutes, seconds, multiplier; + + switch (clockType) { + case FULL_CLOCK_VALUE: + if (!SVGContentUtils::ParseInteger(iter, aEnd, hours) || + !ParseColon(iter, aEnd)) { + return false; + } + MOZ_FALLTHROUGH; + case PARTIAL_CLOCK_VALUE: + if (!ParseSecondsOrMinutes(iter, aEnd, minutes) || + !ParseColon(iter, aEnd) || + !ParseSecondsOrMinutes(iter, aEnd, seconds)) { + return false; + } + if (iter != aEnd && + (*iter != '.' || + !SVGContentUtils::ParseNumber(iter, aEnd, fraction))) { + return false; + } + aResult->SetMillis(nsSMILTime(hours) * MSEC_PER_HOUR + + minutes * MSEC_PER_MIN + + seconds * MSEC_PER_SEC + + NS_round(fraction * MSEC_PER_SEC)); + aIter = iter; + return true; + case TIMECOUNT_VALUE: + if (!SVGContentUtils::ParseInteger(iter, aEnd, timecount)) { + return false; + } + if (iter != aEnd && *iter == '.' && + !SVGContentUtils::ParseNumber(iter, aEnd, fraction)) { + return false; + } + if (!ParseClockMetric(iter, aEnd, multiplier)) { + return false; + } + aResult->SetMillis(nsSMILTime(timecount) * multiplier + + NS_round(fraction * multiplier)); + aIter = iter; + return true; + } + + return false; +} + +bool +ParseOffsetValue(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + nsSMILTimeValue* aResult) +{ + RangedPtr<const char16_t> iter(aIter); + + int32_t sign; + if (!SVGContentUtils::ParseOptionalSign(iter, aEnd, sign) || + !SkipWhitespace(iter, aEnd) || + !ParseClockValue(iter, aEnd, aResult)) { + return false; + } + if (sign == -1) { + aResult->SetMillis(-aResult->GetMillis()); + } + aIter = iter; + return true; +} + +bool +ParseOffsetValue(const nsAString& aSpec, + nsSMILTimeValue* aResult) +{ + RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + const RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + return ParseOffsetValue(iter, end, aResult) && iter == end; +} + +bool +ParseOptionalOffset(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + nsSMILTimeValue* aResult) +{ + if (aIter == aEnd) { + aResult->SetMillis(0L); + return true; + } + + return SkipWhitespace(aIter, aEnd) && + ParseOffsetValue(aIter, aEnd, aResult); +} + +bool +ParseAccessKey(const nsAString& aSpec, nsSMILTimeValueSpecParams& aResult) +{ + MOZ_ASSERT(StringBeginsWith(aSpec, ACCESSKEY_PREFIX_CC) || + StringBeginsWith(aSpec, ACCESSKEY_PREFIX_LC), + "Calling ParseAccessKey on non-accesskey-type spec"); + + nsSMILTimeValueSpecParams result; + result.mType = nsSMILTimeValueSpecParams::ACCESSKEY; + + MOZ_ASSERT(ACCESSKEY_PREFIX_LC.Length() == ACCESSKEY_PREFIX_CC.Length(), + "Case variations for accesskey prefix differ in length"); + + RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + iter += ACCESSKEY_PREFIX_LC.Length(); + + // Expecting at least <accesskey> + ')' + if (end - iter < 2) + return false; + + uint32_t c = *iter++; + + // Process 32-bit codepoints + if (NS_IS_HIGH_SURROGATE(c)) { + if (end - iter < 2) // Expecting at least low-surrogate + ')' + return false; + uint32_t lo = *iter++; + if (!NS_IS_LOW_SURROGATE(lo)) + return false; + c = SURROGATE_TO_UCS4(c, lo); + // XML 1.1 says that 0xFFFE and 0xFFFF are not valid characters + } else if (NS_IS_LOW_SURROGATE(c) || c == 0xFFFE || c == 0xFFFF) { + return false; + } + + result.mRepeatIterationOrAccessKey = c; + + if (*iter++ != ')') + return false; + + if (!ParseOptionalOffset(iter, end, &result.mOffset) || iter != end) { + return false; + } + aResult = result; + return true; +} + +void +MoveToNextToken(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + bool aBreakOnDot, + bool& aIsAnyCharEscaped) +{ + aIsAnyCharEscaped = false; + + bool isCurrentCharEscaped = false; + + while (aIter != aEnd && !IsSVGWhitespace(*aIter)) { + if (isCurrentCharEscaped) { + isCurrentCharEscaped = false; + } else { + if (*aIter == '+' || *aIter == '-' || + (aBreakOnDot && *aIter == '.')) { + break; + } + if (*aIter == '\\') { + isCurrentCharEscaped = true; + aIsAnyCharEscaped = true; + } + } + ++aIter; + } +} + +already_AddRefed<nsIAtom> +ConvertUnescapedTokenToAtom(const nsAString& aToken) +{ + // Whether the token is an id-ref or event-symbol it should be a valid NCName + if (aToken.IsEmpty() || NS_FAILED(nsContentUtils::CheckQName(aToken, false))) + return nullptr; + return NS_Atomize(aToken); +} + +already_AddRefed<nsIAtom> +ConvertTokenToAtom(const nsAString& aToken, + bool aUnescapeToken) +{ + // Unescaping involves making a copy of the string which we'd like to avoid if possible + if (!aUnescapeToken) { + return ConvertUnescapedTokenToAtom(aToken); + } + + nsAutoString token(aToken); + + const char16_t* read = token.BeginReading(); + const char16_t* const end = token.EndReading(); + char16_t* write = token.BeginWriting(); + bool escape = false; + + while (read != end) { + MOZ_ASSERT(write <= read, "Writing past where we've read"); + if (!escape && *read == '\\') { + escape = true; + ++read; + } else { + *write++ = *read++; + escape = false; + } + } + token.Truncate(write - token.BeginReading()); + + return ConvertUnescapedTokenToAtom(token); +} + +bool +ParseElementBaseTimeValueSpec(const nsAString& aSpec, + nsSMILTimeValueSpecParams& aResult) +{ + nsSMILTimeValueSpecParams result; + + // + // The spec will probably look something like one of these + // + // element-name.begin + // element-name.event-name + // event-name + // element-name.repeat(3) + // event\.name + // + // Technically `repeat(3)' is permitted but the behaviour in this case is not + // defined (for SMIL Animation) so we don't support it here. + // + + RangedPtr<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + if (start == end) { + return false; + } + + RangedPtr<const char16_t> tokenEnd(start); + + bool requiresUnescaping; + MoveToNextToken(tokenEnd, end, true, requiresUnescaping); + + RefPtr<nsIAtom> atom = + ConvertTokenToAtom(Substring(start.get(), tokenEnd.get()), + requiresUnescaping); + if (atom == nullptr) { + return false; + } + + // Parse the second token if there is one + if (tokenEnd != end && *tokenEnd == '.') { + result.mDependentElemID = atom; + + ++tokenEnd; + start = tokenEnd; + MoveToNextToken(tokenEnd, end, false, requiresUnescaping); + + const nsAString& token2 = Substring(start.get(), tokenEnd.get()); + + // element-name.begin + if (token2.EqualsLiteral("begin")) { + result.mType = nsSMILTimeValueSpecParams::SYNCBASE; + result.mSyncBegin = true; + // element-name.end + } else if (token2.EqualsLiteral("end")) { + result.mType = nsSMILTimeValueSpecParams::SYNCBASE; + result.mSyncBegin = false; + // element-name.repeat(digit+) + } else if (StringBeginsWith(token2, REPEAT_PREFIX)) { + start += REPEAT_PREFIX.Length(); + int32_t repeatValue; + if (start == tokenEnd || *start == '+' || *start == '-' || + !SVGContentUtils::ParseInteger(start, tokenEnd, repeatValue)) { + return false; + } + if (start == tokenEnd || *start != ')') { + return false; + } + result.mType = nsSMILTimeValueSpecParams::REPEAT; + result.mRepeatIterationOrAccessKey = repeatValue; + // element-name.event-symbol + } else { + atom = ConvertTokenToAtom(token2, requiresUnescaping); + if (atom == nullptr) { + return false; + } + result.mType = nsSMILTimeValueSpecParams::EVENT; + result.mEventSymbol = atom; + } + } else { + // event-symbol + result.mType = nsSMILTimeValueSpecParams::EVENT; + result.mEventSymbol = atom; + } + + // We've reached the end of the token, so we should now be either looking at + // a '+', '-' (possibly with whitespace before it), or the end. + if (!ParseOptionalOffset(tokenEnd, end, &result.mOffset) || tokenEnd != end) { + return false; + } + aResult = result; + return true; +} + +} // namespace + +//------------------------------------------------------------------------------ +// Implementation + +const nsDependentSubstring +nsSMILParserUtils::TrimWhitespace(const nsAString& aString) +{ + nsAString::const_iterator start, end; + + aString.BeginReading(start); + aString.EndReading(end); + + // Skip whitespace characters at the beginning + while (start != end && IsSVGWhitespace(*start)) { + ++start; + } + + // Skip whitespace characters at the end. + while (end != start) { + --end; + + if (!IsSVGWhitespace(*end)) { + // Step back to the last non-whitespace character. + ++end; + + break; + } + } + + return Substring(start, end); +} + +bool +nsSMILParserUtils::ParseKeySplines(const nsAString& aSpec, + FallibleTArray<nsSMILKeySpline>& aKeySplines) +{ + nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> controlPointTokenizer(aSpec, ';'); + while (controlPointTokenizer.hasMoreTokens()) { + + nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> + tokenizer(controlPointTokenizer.nextToken(), ',', + nsCharSeparatedTokenizer::SEPARATOR_OPTIONAL); + + double values[4]; + for (int i = 0 ; i < 4; i++) { + if (!tokenizer.hasMoreTokens() || + !SVGContentUtils::ParseNumber(tokenizer.nextToken(), values[i]) || + values[i] > 1.0 || values[i] < 0.0) { + return false; + } + } + if (tokenizer.hasMoreTokens() || + tokenizer.separatorAfterCurrentToken() || + !aKeySplines.AppendElement(nsSMILKeySpline(values[0], + values[1], + values[2], + values[3]), + fallible)) { + return false; + } + } + + return !aKeySplines.IsEmpty(); +} + +bool +nsSMILParserUtils::ParseSemicolonDelimitedProgressList(const nsAString& aSpec, + bool aNonDecreasing, + FallibleTArray<double>& aArray) +{ + nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> tokenizer(aSpec, ';'); + + double previousValue = -1.0; + + while (tokenizer.hasMoreTokens()) { + double value; + if (!SVGContentUtils::ParseNumber(tokenizer.nextToken(), value)) { + return false; + } + + if (value > 1.0 || value < 0.0 || + (aNonDecreasing && value < previousValue)) { + return false; + } + + if (!aArray.AppendElement(value, fallible)) { + return false; + } + previousValue = value; + } + + return !aArray.IsEmpty(); +} + +// Helper class for ParseValues +class MOZ_STACK_CLASS SMILValueParser : + public nsSMILParserUtils::GenericValueParser +{ +public: + SMILValueParser(const SVGAnimationElement* aSrcElement, + const nsISMILAttr* aSMILAttr, + FallibleTArray<nsSMILValue>* aValuesArray, + bool* aPreventCachingOfSandwich) : + mSrcElement(aSrcElement), + mSMILAttr(aSMILAttr), + mValuesArray(aValuesArray), + mPreventCachingOfSandwich(aPreventCachingOfSandwich) + {} + + virtual bool Parse(const nsAString& aValueStr) override { + nsSMILValue newValue; + bool tmpPreventCachingOfSandwich = false; + if (NS_FAILED(mSMILAttr->ValueFromString(aValueStr, mSrcElement, newValue, + tmpPreventCachingOfSandwich))) + return false; + + if (!mValuesArray->AppendElement(newValue, fallible)) { + return false; + } + if (tmpPreventCachingOfSandwich) { + *mPreventCachingOfSandwich = true; + } + return true; + } +protected: + const SVGAnimationElement* mSrcElement; + const nsISMILAttr* mSMILAttr; + FallibleTArray<nsSMILValue>* mValuesArray; + bool* mPreventCachingOfSandwich; +}; + +bool +nsSMILParserUtils::ParseValues(const nsAString& aSpec, + const SVGAnimationElement* aSrcElement, + const nsISMILAttr& aAttribute, + FallibleTArray<nsSMILValue>& aValuesArray, + bool& aPreventCachingOfSandwich) +{ + // Assume all results can be cached, until we find one that can't. + aPreventCachingOfSandwich = false; + SMILValueParser valueParser(aSrcElement, &aAttribute, + &aValuesArray, &aPreventCachingOfSandwich); + return ParseValuesGeneric(aSpec, valueParser); +} + +bool +nsSMILParserUtils::ParseValuesGeneric(const nsAString& aSpec, + GenericValueParser& aParser) +{ + nsCharSeparatedTokenizerTemplate<IsSVGWhitespace> tokenizer(aSpec, ';'); + if (!tokenizer.hasMoreTokens()) { // Empty list + return false; + } + + while (tokenizer.hasMoreTokens()) { + if (!aParser.Parse(tokenizer.nextToken())) { + return false; + } + } + + return true; +} + +bool +nsSMILParserUtils::ParseRepeatCount(const nsAString& aSpec, + nsSMILRepeatCount& aResult) +{ + const nsAString& spec = + nsSMILParserUtils::TrimWhitespace(aSpec); + + if (spec.EqualsLiteral("indefinite")) { + aResult.SetIndefinite(); + return true; + } + + double value; + if (!SVGContentUtils::ParseNumber(spec, value) || value <= 0.0) { + return false; + } + aResult = value; + return true; +} + +bool +nsSMILParserUtils::ParseTimeValueSpecParams(const nsAString& aSpec, + nsSMILTimeValueSpecParams& aResult) +{ + const nsAString& spec = TrimWhitespace(aSpec); + + if (spec.EqualsLiteral("indefinite")) { + aResult.mType = nsSMILTimeValueSpecParams::INDEFINITE; + return true; + } + + // offset type + if (ParseOffsetValue(spec, &aResult.mOffset)) { + aResult.mType = nsSMILTimeValueSpecParams::OFFSET; + return true; + } + + // wallclock type + if (StringBeginsWith(spec, WALLCLOCK_PREFIX)) { + return false; // Wallclock times not implemented + } + + // accesskey type + if (StringBeginsWith(spec, ACCESSKEY_PREFIX_LC) || + StringBeginsWith(spec, ACCESSKEY_PREFIX_CC)) { + return ParseAccessKey(spec, aResult); + } + + // event, syncbase, or repeat + return ParseElementBaseTimeValueSpec(spec, aResult); +} + +bool +nsSMILParserUtils::ParseClockValue(const nsAString& aSpec, + nsSMILTimeValue* aResult) +{ + RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + return ::ParseClockValue(iter, end, aResult) && iter == end; +} + +int32_t +nsSMILParserUtils::CheckForNegativeNumber(const nsAString& aStr) +{ + int32_t absValLocation = -1; + + RangedPtr<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aStr)); + RangedPtr<const char16_t> iter = start; + RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aStr)); + + // Skip initial whitespace + while (iter != end && IsSVGWhitespace(*iter)) { + ++iter; + } + + // Check for dash + if (iter != end && *iter == '-') { + ++iter; + // Check for numeric character + if (iter != end && SVGContentUtils::IsDigit(*iter)) { + absValLocation = iter - start; + } + } + return absValLocation; +} diff --git a/dom/smil/nsSMILParserUtils.h b/dom/smil/nsSMILParserUtils.h new file mode 100644 index 000000000..c80fd98a2 --- /dev/null +++ b/dom/smil/nsSMILParserUtils.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILPARSERUTILS_H_ +#define NS_SMILPARSERUTILS_H_ + +#include "nsTArray.h" +#include "nsStringFwd.h" + +class nsISMILAttr; +class nsSMILKeySpline; +class nsSMILTimeValue; +class nsSMILValue; +class nsSMILRepeatCount; +class nsSMILTimeValueSpecParams; + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +/** + * Common parsing utilities for the SMIL module. There is little re-use here; it + * simply serves to simplify other classes by moving parsing outside and to aid + * unit testing. + */ +class nsSMILParserUtils +{ +public: + // Abstract helper-class for assisting in parsing |values| attribute + class MOZ_STACK_CLASS GenericValueParser { + public: + virtual bool Parse(const nsAString& aValueStr) = 0; + }; + + static const nsDependentSubstring TrimWhitespace(const nsAString& aString); + + static bool ParseKeySplines(const nsAString& aSpec, + FallibleTArray<nsSMILKeySpline>& aKeySplines); + + // Used for parsing the |keyTimes| and |keyPoints| attributes. + static bool ParseSemicolonDelimitedProgressList(const nsAString& aSpec, + bool aNonDecreasing, + FallibleTArray<double>& aArray); + + static bool ParseValues(const nsAString& aSpec, + const mozilla::dom::SVGAnimationElement* aSrcElement, + const nsISMILAttr& aAttribute, + FallibleTArray<nsSMILValue>& aValuesArray, + bool& aPreventCachingOfSandwich); + + // Generic method that will run some code on each sub-section of an animation + // element's "values" list. + static bool ParseValuesGeneric(const nsAString& aSpec, + GenericValueParser& aParser); + + static bool ParseRepeatCount(const nsAString& aSpec, + nsSMILRepeatCount& aResult); + + static bool ParseTimeValueSpecParams(const nsAString& aSpec, + nsSMILTimeValueSpecParams& aResult); + + /* + * Parses a clock value as defined in the SMIL Animation specification. + * If parsing succeeds the returned value will be a non-negative, definite + * time value i.e. IsDefinite will return true. + * + * @param aSpec The string containing a clock value, e.g. "10s" + * @param aResult The parsed result. [OUT] + * @return true if parsing succeeded, otherwise false. + */ + static bool ParseClockValue(const nsAString& aSpec, + nsSMILTimeValue* aResult); + + /* + * This method checks whether the given string looks like a negative number. + * Specifically, it checks whether the string looks matches the pattern + * "[whitespace]*-[numeral].*" If the string matches this pattern, this + * method returns the index of the first character after the '-' sign + * (i.e. the index of the absolute value). If not, this method returns -1. + */ + static int32_t CheckForNegativeNumber(const nsAString& aStr); +}; + +#endif // NS_SMILPARSERUTILS_H_ diff --git a/dom/smil/nsSMILRepeatCount.cpp b/dom/smil/nsSMILRepeatCount.cpp new file mode 100644 index 000000000..d5c61fe53 --- /dev/null +++ b/dom/smil/nsSMILRepeatCount.cpp @@ -0,0 +1,10 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILRepeatCount.h" + +/*static*/ const double nsSMILRepeatCount::kNotSet = -1.0; +/*static*/ const double nsSMILRepeatCount::kIndefinite = -2.0; diff --git a/dom/smil/nsSMILRepeatCount.h b/dom/smil/nsSMILRepeatCount.h new file mode 100644 index 000000000..be36badee --- /dev/null +++ b/dom/smil/nsSMILRepeatCount.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef nsSMILRepeatCount_h +#define nsSMILRepeatCount_h + +#include "nsDebug.h" +#include <math.h> + +//---------------------------------------------------------------------- +// nsSMILRepeatCount +// +// A tri-state non-negative floating point number for representing the number of +// times an animation repeat, i.e. the SMIL repeatCount attribute. +// +// The three states are: +// 1. not-set +// 2. set (with non-negative, non-zero count value) +// 3. indefinite +// +class nsSMILRepeatCount +{ +public: + nsSMILRepeatCount() : mCount(kNotSet) {} + explicit nsSMILRepeatCount(double aCount) + : mCount(kNotSet) { SetCount(aCount); } + + operator double() const { + MOZ_ASSERT(IsDefinite(), + "Converting indefinite or unset repeat count to double"); + return mCount; + } + bool IsDefinite() const { + return mCount != kNotSet && mCount != kIndefinite; + } + bool IsIndefinite() const { return mCount == kIndefinite; } + bool IsSet() const { return mCount != kNotSet; } + + nsSMILRepeatCount& operator=(double aCount) + { + SetCount(aCount); + return *this; + } + void SetCount(double aCount) + { + NS_ASSERTION(aCount > 0.0, "Negative or zero repeat count"); + mCount = aCount > 0.0 ? aCount : kNotSet; + } + void SetIndefinite() { mCount = kIndefinite; } + void Unset() { mCount = kNotSet; } + +private: + static const double kNotSet; + static const double kIndefinite; + + double mCount; +}; + +#endif diff --git a/dom/smil/nsSMILSetAnimationFunction.cpp b/dom/smil/nsSMILSetAnimationFunction.cpp new file mode 100644 index 000000000..d636f9aff --- /dev/null +++ b/dom/smil/nsSMILSetAnimationFunction.cpp @@ -0,0 +1,101 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILSetAnimationFunction.h" + +inline bool +nsSMILSetAnimationFunction::IsDisallowedAttribute( + const nsIAtom* aAttribute) const +{ + // + // A <set> element is similar to <animate> but lacks: + // AnimationValue.attrib(calcMode, values, keyTimes, keySplines, from, to, + // by) -- BUT has 'to' + // AnimationAddition.attrib(additive, accumulate) + // + if (aAttribute == nsGkAtoms::calcMode || + aAttribute == nsGkAtoms::values || + aAttribute == nsGkAtoms::keyTimes || + aAttribute == nsGkAtoms::keySplines || + aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::by || + aAttribute == nsGkAtoms::additive || + aAttribute == nsGkAtoms::accumulate) { + return true; + } + + return false; +} + +bool +nsSMILSetAnimationFunction::SetAttr(nsIAtom* aAttribute, + const nsAString& aValue, + nsAttrValue& aResult, + nsresult* aParseResult) +{ + if (IsDisallowedAttribute(aAttribute)) { + aResult.SetTo(aValue); + if (aParseResult) { + // SMILANIM 4.2 says: + // + // The additive and accumulate attributes are not allowed, and will be + // ignored if specified. + // + // So at least for those two attributes we shouldn't report an error even + // if they're present. For now we'll also just silently ignore other + // attribute types too. + *aParseResult = NS_OK; + } + return true; + } + + return nsSMILAnimationFunction::SetAttr(aAttribute, aValue, + aResult, aParseResult); +} + +bool +nsSMILSetAnimationFunction::UnsetAttr(nsIAtom* aAttribute) +{ + if (IsDisallowedAttribute(aAttribute)) { + return true; + } + + return nsSMILAnimationFunction::UnsetAttr(aAttribute); +} + +bool +nsSMILSetAnimationFunction::HasAttr(nsIAtom* aAttName) const +{ + if (IsDisallowedAttribute(aAttName)) + return false; + + return nsSMILAnimationFunction::HasAttr(aAttName); +} + +const nsAttrValue* +nsSMILSetAnimationFunction::GetAttr(nsIAtom* aAttName) const +{ + if (IsDisallowedAttribute(aAttName)) + return nullptr; + + return nsSMILAnimationFunction::GetAttr(aAttName); +} + +bool +nsSMILSetAnimationFunction::GetAttr(nsIAtom* aAttName, + nsAString& aResult) const +{ + if (IsDisallowedAttribute(aAttName)) + return false; + + return nsSMILAnimationFunction::GetAttr(aAttName, aResult); +} + +bool +nsSMILSetAnimationFunction::WillReplace() const +{ + return true; +} diff --git a/dom/smil/nsSMILSetAnimationFunction.h b/dom/smil/nsSMILSetAnimationFunction.h new file mode 100644 index 000000000..6401a3bf5 --- /dev/null +++ b/dom/smil/nsSMILSetAnimationFunction.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILSETANIMATIONFUNCTION_H_ +#define NS_SMILSETANIMATIONFUNCTION_H_ + +#include "mozilla/Attributes.h" +#include "nsSMILAnimationFunction.h" + +//---------------------------------------------------------------------- +// nsSMILSetAnimationFunction +// +// Subclass of nsSMILAnimationFunction that limits the behaviour to that offered +// by a <set> element. +// +class nsSMILSetAnimationFunction : public nsSMILAnimationFunction +{ +public: + /* + * Sets animation-specific attributes (or marks them dirty, in the case + * of from/to/by/values). + * + * @param aAttribute The attribute being set + * @param aValue The updated value of the attribute. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aParseResult Outparam used for reporting parse errors. Will be set + * to NS_OK if everything succeeds. + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, nsresult* aParseResult = nullptr) override; + + /* + * Unsets the given attribute. + * + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool UnsetAttr(nsIAtom* aAttribute) override; + +protected: + // Although <set> animation might look like to-animation, unlike to-animation, + // it never interpolates values. + // Returning false here will mean this animation function gets treated as + // a single-valued function and no interpolation will be attempted. + virtual bool IsToAnimation() const override { + return false; + } + + // <set> applies the exact same value across the simple duration. + virtual bool IsValueFixedForSimpleDuration() const override { + return true; + } + virtual bool HasAttr(nsIAtom* aAttName) const override; + virtual const nsAttrValue* GetAttr(nsIAtom* aAttName) const override; + virtual bool GetAttr(nsIAtom* aAttName, + nsAString& aResult) const override; + virtual bool WillReplace() const override; + + bool IsDisallowedAttribute(const nsIAtom* aAttribute) const; +}; + +#endif // NS_SMILSETANIMATIONFUNCTION_H_ diff --git a/dom/smil/nsSMILTargetIdentifier.h b/dom/smil/nsSMILTargetIdentifier.h new file mode 100644 index 000000000..56fb08f34 --- /dev/null +++ b/dom/smil/nsSMILTargetIdentifier.h @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILTARGETIDENTIFIER_H_ +#define NS_SMILTARGETIDENTIFIER_H_ + +#include "mozilla/dom/Element.h" + +/** + * Struct: nsSMILTargetIdentifier + * + * Tuple of: { Animated Element, Attribute Name, Attribute Type (CSS vs. XML) } + * + * Used in nsSMILAnimationController as hash key for mapping an animation + * target to the nsSMILCompositor for that target. + * + * NOTE: Need a nsRefPtr for the element & attribute name, because + * nsSMILAnimationController retain its hash table for one sample into the + * future, and we need to make sure their target isn't deleted in that time. + */ + +struct nsSMILTargetIdentifier +{ + nsSMILTargetIdentifier() + : mElement(nullptr), mAttributeName(nullptr), + mAttributeNamespaceID(kNameSpaceID_Unknown), mIsCSS(false) {} + + inline bool Equals(const nsSMILTargetIdentifier& aOther) const + { + return (aOther.mElement == mElement && + aOther.mAttributeName == mAttributeName && + aOther.mAttributeNamespaceID == mAttributeNamespaceID && + aOther.mIsCSS == mIsCSS); + } + + RefPtr<mozilla::dom::Element> mElement; + RefPtr<nsIAtom> mAttributeName; + int32_t mAttributeNamespaceID; + bool mIsCSS; +}; + +/** + * Class: nsSMILWeakTargetIdentifier + * + * Version of the above struct that uses non-owning pointers. These are kept + * private, to ensure that they aren't ever dereferenced (or used at all, + * outside of Equals()). + * + * This is solely for comparisons to determine if a target has changed + * from one sample to the next. + */ +class nsSMILWeakTargetIdentifier +{ +public: + // Trivial constructor + nsSMILWeakTargetIdentifier() + : mElement(nullptr), mAttributeName(nullptr), mIsCSS(false) {} + + // Allow us to update a weak identifier to match a given non-weak identifier + nsSMILWeakTargetIdentifier& + operator=(const nsSMILTargetIdentifier& aOther) + { + mElement = aOther.mElement; + mAttributeName = aOther.mAttributeName; + mIsCSS = aOther.mIsCSS; + return *this; + } + + // Allow for comparison vs. non-weak identifier + inline bool Equals(const nsSMILTargetIdentifier& aOther) const + { + return (aOther.mElement == mElement && + aOther.mAttributeName == mAttributeName && + aOther.mIsCSS == mIsCSS); + } + +private: + const nsIContent* mElement; + const nsIAtom* mAttributeName; + bool mIsCSS; +}; + +#endif // NS_SMILTARGETIDENTIFIER_H_ diff --git a/dom/smil/nsSMILTimeContainer.cpp b/dom/smil/nsSMILTimeContainer.cpp new file mode 100644 index 000000000..3df8a64ca --- /dev/null +++ b/dom/smil/nsSMILTimeContainer.cpp @@ -0,0 +1,339 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILTimeContainer.h" +#include "nsSMILTimeValue.h" +#include "nsSMILTimedElement.h" +#include <algorithm> + +#include "mozilla/AutoRestore.h" + +nsSMILTimeContainer::nsSMILTimeContainer() +: + mParent(nullptr), + mCurrentTime(0L), + mParentOffset(0L), + mPauseStart(0L), + mNeedsPauseSample(false), + mNeedsRewind(false), + mIsSeeking(false), + mHoldingEntries(false), + mPauseState(PAUSE_BEGIN) +{ +} + +nsSMILTimeContainer::~nsSMILTimeContainer() +{ + if (mParent) { + mParent->RemoveChild(*this); + } +} + +nsSMILTimeValue +nsSMILTimeContainer::ContainerToParentTime(nsSMILTime aContainerTime) const +{ + // If we're paused, then future times are indefinite + if (IsPaused() && aContainerTime > mCurrentTime) + return nsSMILTimeValue::Indefinite(); + + return nsSMILTimeValue(aContainerTime + mParentOffset); +} + +nsSMILTimeValue +nsSMILTimeContainer::ParentToContainerTime(nsSMILTime aParentTime) const +{ + // If we're paused, then any time after when we paused is indefinite + if (IsPaused() && aParentTime > mPauseStart) + return nsSMILTimeValue::Indefinite(); + + return nsSMILTimeValue(aParentTime - mParentOffset); +} + +void +nsSMILTimeContainer::Begin() +{ + Resume(PAUSE_BEGIN); + if (mPauseState) { + mNeedsPauseSample = true; + } + + // This is a little bit complicated here. Ideally we'd just like to call + // Sample() and force an initial sample but this turns out to be a bad idea + // because this may mean that NeedsSample() no longer reports true and so when + // we come to the first real sample our parent will skip us over altogether. + // So we force the time to be updated and adopt the policy to never call + // Sample() ourselves but to always leave that to our parent or client. + + UpdateCurrentTime(); +} + +void +nsSMILTimeContainer::Pause(uint32_t aType) +{ + bool didStartPause = false; + + if (!mPauseState && aType) { + mPauseStart = GetParentTime(); + mNeedsPauseSample = true; + didStartPause = true; + } + + mPauseState |= aType; + + if (didStartPause) { + NotifyTimeChange(); + } +} + +void +nsSMILTimeContainer::Resume(uint32_t aType) +{ + if (!mPauseState) + return; + + mPauseState &= ~aType; + + if (!mPauseState) { + nsSMILTime extraOffset = GetParentTime() - mPauseStart; + mParentOffset += extraOffset; + NotifyTimeChange(); + } +} + +nsSMILTime +nsSMILTimeContainer::GetCurrentTime() const +{ + // The following behaviour is consistent with: + // http://www.w3.org/2003/01/REC-SVG11-20030114-errata + // #getCurrentTime_setCurrentTime_undefined_before_document_timeline_begin + // which says that if GetCurrentTime is called before the document timeline + // has begun we should just return 0. + if (IsPausedByType(PAUSE_BEGIN)) + return 0L; + + return mCurrentTime; +} + +void +nsSMILTimeContainer::SetCurrentTime(nsSMILTime aSeekTo) +{ + // SVG 1.1 doesn't specify what to do for negative times so we adopt SVGT1.2's + // behaviour of clamping negative times to 0. + aSeekTo = std::max<nsSMILTime>(0, aSeekTo); + + // The following behaviour is consistent with: + // http://www.w3.org/2003/01/REC-SVG11-20030114-errata + // #getCurrentTime_setCurrentTime_undefined_before_document_timeline_begin + // which says that if SetCurrentTime is called before the document timeline + // has begun we should still adjust the offset. + nsSMILTime parentTime = GetParentTime(); + mParentOffset = parentTime - aSeekTo; + mIsSeeking = true; + + if (IsPaused()) { + mNeedsPauseSample = true; + mPauseStart = parentTime; + } + + if (aSeekTo < mCurrentTime) { + // Backwards seek + mNeedsRewind = true; + ClearMilestones(); + } + + // Force an update to the current time in case we get a call to GetCurrentTime + // before another call to Sample(). + UpdateCurrentTime(); + + NotifyTimeChange(); +} + +nsSMILTime +nsSMILTimeContainer::GetParentTime() const +{ + if (mParent) + return mParent->GetCurrentTime(); + + return 0L; +} + +void +nsSMILTimeContainer::SyncPauseTime() +{ + if (IsPaused()) { + nsSMILTime parentTime = GetParentTime(); + nsSMILTime extraOffset = parentTime - mPauseStart; + mParentOffset += extraOffset; + mPauseStart = parentTime; + } +} + +void +nsSMILTimeContainer::Sample() +{ + if (!NeedsSample()) + return; + + UpdateCurrentTime(); + DoSample(); + + mNeedsPauseSample = false; +} + +nsresult +nsSMILTimeContainer::SetParent(nsSMILTimeContainer* aParent) +{ + if (mParent) { + mParent->RemoveChild(*this); + // When we're not attached to a parent time container, GetParentTime() will + // return 0. We need to adjust our pause state information to be relative to + // this new time base. + // Note that since "current time = parent time - parent offset" setting the + // parent offset and pause start as follows preserves our current time even + // while parent time = 0. + mParentOffset = -mCurrentTime; + mPauseStart = 0L; + } + + mParent = aParent; + + nsresult rv = NS_OK; + if (mParent) { + rv = mParent->AddChild(*this); + } + + return rv; +} + +bool +nsSMILTimeContainer::AddMilestone(const nsSMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement) +{ + // We record the milestone time and store it along with the element but this + // time may change (e.g. if attributes are changed on the timed element in + // between samples). If this happens, then we may do an unecessary sample + // but that's pretty cheap. + MOZ_RELEASE_ASSERT(!mHoldingEntries); + return mMilestoneEntries.Push(MilestoneEntry(aMilestone, aElement)); +} + +void +nsSMILTimeContainer::ClearMilestones() +{ + MOZ_RELEASE_ASSERT(!mHoldingEntries); + mMilestoneEntries.Clear(); +} + +bool +nsSMILTimeContainer::GetNextMilestoneInParentTime( + nsSMILMilestone& aNextMilestone) const +{ + if (mMilestoneEntries.IsEmpty()) + return false; + + nsSMILTimeValue parentTime = + ContainerToParentTime(mMilestoneEntries.Top().mMilestone.mTime); + if (!parentTime.IsDefinite()) + return false; + + aNextMilestone = nsSMILMilestone(parentTime.GetMillis(), + mMilestoneEntries.Top().mMilestone.mIsEnd); + + return true; +} + +bool +nsSMILTimeContainer::PopMilestoneElementsAtMilestone( + const nsSMILMilestone& aMilestone, + AnimElemArray& aMatchedElements) +{ + if (mMilestoneEntries.IsEmpty()) + return false; + + nsSMILTimeValue containerTime = ParentToContainerTime(aMilestone.mTime); + if (!containerTime.IsDefinite()) + return false; + + nsSMILMilestone containerMilestone(containerTime.GetMillis(), + aMilestone.mIsEnd); + + MOZ_ASSERT(mMilestoneEntries.Top().mMilestone >= containerMilestone, + "Trying to pop off earliest times but we have earlier ones that " + "were overlooked"); + + MOZ_RELEASE_ASSERT(!mHoldingEntries); + + bool gotOne = false; + while (!mMilestoneEntries.IsEmpty() && + mMilestoneEntries.Top().mMilestone == containerMilestone) + { + aMatchedElements.AppendElement(mMilestoneEntries.Pop().mTimebase); + gotOne = true; + } + + return gotOne; +} + +void +nsSMILTimeContainer::Traverse(nsCycleCollectionTraversalCallback* aCallback) +{ + AutoRestore<bool> saveHolding(mHoldingEntries); + mHoldingEntries = true; + const MilestoneEntry* p = mMilestoneEntries.Elements(); + while (p < mMilestoneEntries.Elements() + mMilestoneEntries.Length()) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCallback, "mTimebase"); + aCallback->NoteXPCOMChild(static_cast<nsIContent*>(p->mTimebase.get())); + ++p; + } +} + +void +nsSMILTimeContainer::Unlink() +{ + MOZ_RELEASE_ASSERT(!mHoldingEntries); + mMilestoneEntries.Clear(); +} + +void +nsSMILTimeContainer::UpdateCurrentTime() +{ + nsSMILTime now = IsPaused() ? mPauseStart : GetParentTime(); + mCurrentTime = now - mParentOffset; + MOZ_ASSERT(mCurrentTime >= 0, "Container has negative time"); +} + +void +nsSMILTimeContainer::NotifyTimeChange() +{ + // Called when the container time is changed with respect to the document + // time. When this happens time dependencies in other time containers need to + // re-resolve their times because begin and end times are stored in container + // time. + // + // To get the list of timed elements with dependencies we simply re-use the + // milestone elements. This is because any timed element with dependents and + // with significant transitions yet to fire should have their next milestone + // registered. Other timed elements don't matter. + + // Copy the timed elements to a separate array before calling + // HandleContainerTimeChange on each of them in case doing so mutates + // mMilestoneEntries. + nsTArray<RefPtr<mozilla::dom::SVGAnimationElement>> elems; + + { + AutoRestore<bool> saveHolding(mHoldingEntries); + mHoldingEntries = true; + for (const MilestoneEntry* p = mMilestoneEntries.Elements(); + p < mMilestoneEntries.Elements() + mMilestoneEntries.Length(); + ++p) { + elems.AppendElement(p->mTimebase.get()); + } + } + + for (auto& elem : elems) { + elem->TimedElement().HandleContainerTimeChange(); + } +} diff --git a/dom/smil/nsSMILTimeContainer.h b/dom/smil/nsSMILTimeContainer.h new file mode 100644 index 000000000..50c9709db --- /dev/null +++ b/dom/smil/nsSMILTimeContainer.h @@ -0,0 +1,298 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILTIMECONTAINER_H_ +#define NS_SMILTIMECONTAINER_H_ + +#include "mozilla/dom/SVGAnimationElement.h" +#include "nscore.h" +#include "nsSMILTypes.h" +#include "nsTPriorityQueue.h" +#include "nsSMILMilestone.h" + +class nsSMILTimeValue; + +//---------------------------------------------------------------------- +// nsSMILTimeContainer +// +// Common base class for a time base that can be paused, resumed, and sampled. +// +class nsSMILTimeContainer +{ +public: + nsSMILTimeContainer(); + virtual ~nsSMILTimeContainer(); + + /* + * Pause request types. + */ + enum { + PAUSE_BEGIN = 1, // Paused because timeline has yet to begin. + PAUSE_SCRIPT = 2, // Paused by script. + PAUSE_PAGEHIDE = 4, // Paused because our doc is hidden. + PAUSE_USERPREF = 8, // Paused because animations are disabled in prefs. + PAUSE_IMAGE = 16 // Paused becuase we're in an image that's suspended. + }; + + /* + * Cause the time container to record its begin time. + */ + void Begin(); + + /* + * Pause this time container + * + * @param aType The source of the pause request. Successive calls to Pause + * with the same aType will be ignored. The container will remain paused until + * each call to Pause of a given aType has been matched by at least one call + * to Resume with the same aType. + */ + virtual void Pause(uint32_t aType); + + /* + * Resume this time container + * + * param @aType The source of the resume request. Clears the pause flag for + * this particular type of pause request. When all pause flags have been + * cleared the time container will be resumed. + */ + virtual void Resume(uint32_t aType); + + /** + * Returns true if this time container is paused by the specified type. + * Note that the time container may also be paused by other types; this method + * does not test if aType is the exclusive pause source. + * + * @param @aType The pause source to test for. + * @return true if this container is paused by aType. + */ + bool IsPausedByType(uint32_t aType) const { return mPauseState & aType; } + + /** + * Returns true if this time container is paused. + * Generally you should test for a specific type of pausing using + * IsPausedByType. + * + * @return true if this container is paused, false otherwise. + */ + bool IsPaused() const { return mPauseState != 0; } + + /* + * Return the time elapsed since this time container's begin time (expressed + * in parent time) minus any accumulated offset from pausing. + */ + nsSMILTime GetCurrentTime() const; + + /* + * Seek the document timeline to the specified time. + * + * @param aSeekTo The time to seek to, expressed in this time container's time + * base (i.e. the same units as GetCurrentTime). + */ + void SetCurrentTime(nsSMILTime aSeekTo); + + /* + * Return the current time for the parent time container if any. + */ + virtual nsSMILTime GetParentTime() const; + + /* + * Convert container time to parent time. + * + * @param aContainerTime The container time to convert. + * @return The equivalent parent time or indefinite if the container is + * paused and the time is in the future. + */ + nsSMILTimeValue ContainerToParentTime(nsSMILTime aContainerTime) const; + + /* + * Convert from parent time to container time. + * + * @param aParentTime The parent time to convert. + * @return The equivalent container time or indefinite if the container is + * paused and aParentTime is after the time when the pause began. + */ + nsSMILTimeValue ParentToContainerTime(nsSMILTime aParentTime) const; + + /* + * If the container is paused, causes the pause time to be updated to the + * current parent time. This should be called before updating + * cross-container dependencies that will call ContainerToParentTime in order + * to provide more intuitive results. + */ + void SyncPauseTime(); + + /* + * Updates the current time of this time container and calls DoSample to + * perform any sample-operations. + */ + void Sample(); + + /* + * Return if this time container should be sampled or can be skipped. + * + * This is most useful as an optimisation for skipping time containers that + * don't require a sample. + */ + bool NeedsSample() const { return !mPauseState || mNeedsPauseSample; } + + /* + * Indicates if the elements of this time container need to be rewound. + * This occurs during a backwards seek. + */ + bool NeedsRewind() const { return mNeedsRewind; } + void ClearNeedsRewind() { mNeedsRewind = false; } + + /* + * Indicates the time container is currently processing a SetCurrentTime + * request and appropriate seek behaviour should be applied by child elements + * (e.g. not firing time events). + */ + bool IsSeeking() const { return mIsSeeking; } + void MarkSeekFinished() { mIsSeeking = false; } + + /* + * Sets the parent time container. + * + * The callee still retains ownership of the time container. + */ + nsresult SetParent(nsSMILTimeContainer* aParent); + + /* + * Registers an element for a sample at the given time. + * + * @param aMilestone The milestone to register in container time. + * @param aElement The timebase element that needs a sample at + * aMilestone. + * @return true if the element was successfully added, false otherwise. + */ + bool AddMilestone(const nsSMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement); + + /* + * Resets the list of milestones. + */ + void ClearMilestones(); + + /* + * Returns the next significant transition from amongst the registered + * milestones. + * + * @param[out] aNextMilestone The next milestone with time in parent time. + * + * @return true if there exists another milestone, false otherwise in + * which case aNextMilestone will be unmodified. + */ + bool GetNextMilestoneInParentTime(nsSMILMilestone& aNextMilestone) const; + + typedef nsTArray<RefPtr<mozilla::dom::SVGAnimationElement> > AnimElemArray; + + /* + * Removes and returns the timebase elements from the start of the list of + * timebase elements that match the given time. + * + * @param aMilestone The milestone time to match in parent time. This + * must be <= GetNextMilestoneInParentTime. + * @param[out] aMatchedElements The array to which matching elements will be + * appended. + * @return true if one or more elements match, false otherwise. + */ + bool PopMilestoneElementsAtMilestone(const nsSMILMilestone& aMilestone, + AnimElemArray& aMatchedElements); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + +protected: + /* + * Per-sample operations to be performed whenever Sample() is called and + * NeedsSample() is true. Called after updating mCurrentTime; + */ + virtual void DoSample() { } + + /* + * Adding and removing child containers is not implemented in the base class + * because not all subclasses need this. + */ + + /* + * Adds a child time container. + */ + virtual nsresult AddChild(nsSMILTimeContainer& aChild) + { + return NS_ERROR_FAILURE; + } + + /* + * Removes a child time container. + */ + virtual void RemoveChild(nsSMILTimeContainer& aChild) { } + + /* + * Implementation helper to update the current time. + */ + void UpdateCurrentTime(); + + /* + * Implementation helper to notify timed elements with dependencies that the + * container time has changed with respect to the document time. + */ + void NotifyTimeChange(); + + // The parent time container, if any + nsSMILTimeContainer* mParent; + + // The current time established at the last call to Sample() + nsSMILTime mCurrentTime; + + // The number of milliseconds for which the container has been paused + // (excluding the current pause interval if the container is currently + // paused). + // + // Current time = parent time - mParentOffset + // + nsSMILTime mParentOffset; + + // The timestamp in parent time when the container was paused + nsSMILTime mPauseStart; + + // Whether or not a pause sample is required + bool mNeedsPauseSample; + + bool mNeedsRewind; // Backwards seek performed + bool mIsSeeking; // Currently in the middle of a seek operation + + bool mHoldingEntries; // True if there's a raw pointer to mMilestoneEntries on the stack. + + // A bitfield of the pause state for all pause requests + uint32_t mPauseState; + + struct MilestoneEntry + { + MilestoneEntry(nsSMILMilestone aMilestone, + mozilla::dom::SVGAnimationElement& aElement) + : mMilestone(aMilestone), mTimebase(&aElement) + { } + + bool operator<(const MilestoneEntry& aOther) const + { + return mMilestone < aOther.mMilestone; + } + + nsSMILMilestone mMilestone; // In container time. + RefPtr<mozilla::dom::SVGAnimationElement> mTimebase; + }; + + // Queue of elements with registered milestones. Used to update the model with + // significant transitions that occur between two samples. Since timed element + // re-register their milestones when they're sampled this is reset once we've + // taken care of the milestones before the current sample time but before we + // actually do the full sample. + nsTPriorityQueue<MilestoneEntry> mMilestoneEntries; +}; + +#endif // NS_SMILTIMECONTAINER_H_ diff --git a/dom/smil/nsSMILTimeValue.cpp b/dom/smil/nsSMILTimeValue.cpp new file mode 100644 index 000000000..ddd5b3ce7 --- /dev/null +++ b/dom/smil/nsSMILTimeValue.cpp @@ -0,0 +1,41 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILTimeValue.h" + +nsSMILTime nsSMILTimeValue::kUnresolvedMillis = INT64_MAX; + +//---------------------------------------------------------------------- +// nsSMILTimeValue methods: + +static inline int8_t +Cmp(int64_t aA, int64_t aB) +{ + return aA == aB ? 0 : (aA > aB ? 1 : -1); +} + +int8_t +nsSMILTimeValue::CompareTo(const nsSMILTimeValue& aOther) const +{ + int8_t result; + + if (mState == STATE_DEFINITE) { + result = (aOther.mState == STATE_DEFINITE) + ? Cmp(mMilliseconds, aOther.mMilliseconds) + : -1; + } else if (mState == STATE_INDEFINITE) { + if (aOther.mState == STATE_DEFINITE) + result = 1; + else if (aOther.mState == STATE_INDEFINITE) + result = 0; + else + result = -1; + } else { + result = (aOther.mState != STATE_UNRESOLVED) ? 1 : 0; + } + + return result; +} diff --git a/dom/smil/nsSMILTimeValue.h b/dom/smil/nsSMILTimeValue.h new file mode 100644 index 000000000..de694bdbf --- /dev/null +++ b/dom/smil/nsSMILTimeValue.h @@ -0,0 +1,135 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILTIMEVALUE_H_ +#define NS_SMILTIMEVALUE_H_ + +#include "nsSMILTypes.h" +#include "nsDebug.h" + +/*---------------------------------------------------------------------- + * nsSMILTimeValue class + * + * A tri-state time value. + * + * First a quick overview of the SMIL time data types: + * + * nsSMILTime -- a timestamp in milliseconds. + * nsSMILTimeValue -- (this class) a timestamp that can take the additional + * states 'indefinite' and 'unresolved' + * nsSMILInstanceTime -- an nsSMILTimeValue used for constructing intervals. It + * contains additional fields to govern reset behavior + * and track timing dependencies (e.g. syncbase timing). + * nsSMILInterval -- a pair of nsSMILInstanceTimes that defines a begin and + * an end time for animation. + * nsSMILTimeValueSpec -- a component of a begin or end attribute, such as the + * '5s' or 'a.end+2m' in begin="5s; a.end+2m". Acts as + * a broker between an nsSMILTimedElement and its + * nsSMILInstanceTimes by generating new instance times + * and handling changes to existing times. + * + * Objects of this class may be in one of three states: + * + * 1) The time is resolved and has a definite millisecond value + * 2) The time is resolved and indefinite + * 3) The time is unresolved + * + * In summary: + * + * State | GetMillis | IsDefinite | IsIndefinite | IsResolved + * -----------+-----------------+------------+--------------+------------ + * Definite | nsSMILTimeValue | true | false | true + * -----------+-----------------+------------+--------------+------------ + * Indefinite | -- | false | true | true + * -----------+-----------------+------------+--------------+------------ + * Unresolved | -- | false | false | false + * + */ + +class nsSMILTimeValue +{ +public: + // Creates an unresolved time value + nsSMILTimeValue() + : mMilliseconds(kUnresolvedMillis), + mState(STATE_UNRESOLVED) + { } + + // Creates a resolved time value + explicit nsSMILTimeValue(nsSMILTime aMillis) + : mMilliseconds(aMillis), + mState(STATE_DEFINITE) + { } + + // Named constructor to create an indefinite time value + static nsSMILTimeValue Indefinite() + { + nsSMILTimeValue value; + value.SetIndefinite(); + return value; + } + + bool IsIndefinite() const { return mState == STATE_INDEFINITE; } + void SetIndefinite() + { + mState = STATE_INDEFINITE; + mMilliseconds = kUnresolvedMillis; + } + + bool IsResolved() const { return mState != STATE_UNRESOLVED; } + void SetUnresolved() + { + mState = STATE_UNRESOLVED; + mMilliseconds = kUnresolvedMillis; + } + + bool IsDefinite() const { return mState == STATE_DEFINITE; } + nsSMILTime GetMillis() const + { + MOZ_ASSERT(mState == STATE_DEFINITE, + "GetMillis() called for unresolved or indefinite time"); + + return mState == STATE_DEFINITE ? mMilliseconds : kUnresolvedMillis; + } + + void SetMillis(nsSMILTime aMillis) + { + mState = STATE_DEFINITE; + mMilliseconds = aMillis; + } + + int8_t CompareTo(const nsSMILTimeValue& aOther) const; + + bool operator==(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) == 0; } + + bool operator!=(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) != 0; } + + bool operator<(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) < 0; } + + bool operator>(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) > 0; } + + bool operator<=(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) <= 0; } + + bool operator>=(const nsSMILTimeValue& aOther) const + { return CompareTo(aOther) >= 0; } + +private: + static nsSMILTime kUnresolvedMillis; + + nsSMILTime mMilliseconds; + enum { + STATE_DEFINITE, + STATE_INDEFINITE, + STATE_UNRESOLVED + } mState; +}; + +#endif // NS_SMILTIMEVALUE_H_ diff --git a/dom/smil/nsSMILTimeValueSpec.cpp b/dom/smil/nsSMILTimeValueSpec.cpp new file mode 100644 index 000000000..6948b5d58 --- /dev/null +++ b/dom/smil/nsSMILTimeValueSpec.cpp @@ -0,0 +1,536 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/EventListenerManager.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsSMILTimeValueSpec.h" +#include "nsSMILInterval.h" +#include "nsSMILTimeContainer.h" +#include "nsSMILTimeValue.h" +#include "nsSMILTimedElement.h" +#include "nsSMILInstanceTime.h" +#include "nsSMILParserUtils.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMTimeEvent.h" +#include "nsString.h" +#include <limits> + +using namespace mozilla; +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// Nested class: EventListener + +NS_IMPL_ISUPPORTS(nsSMILTimeValueSpec::EventListener, nsIDOMEventListener) + +NS_IMETHODIMP +nsSMILTimeValueSpec::EventListener::HandleEvent(nsIDOMEvent* aEvent) +{ + if (mSpec) { + mSpec->HandleEvent(aEvent); + } + return NS_OK; +} + +//---------------------------------------------------------------------- +// Implementation + +nsSMILTimeValueSpec::nsSMILTimeValueSpec(nsSMILTimedElement& aOwner, + bool aIsBegin) + : mOwner(&aOwner), + mIsBegin(aIsBegin), + mReferencedElement(this) +{ +} + +nsSMILTimeValueSpec::~nsSMILTimeValueSpec() +{ + UnregisterFromReferencedElement(mReferencedElement.get()); + if (mEventListener) { + mEventListener->Disconnect(); + mEventListener = nullptr; + } +} + +nsresult +nsSMILTimeValueSpec::SetSpec(const nsAString& aStringSpec, + Element* aContextNode) +{ + nsSMILTimeValueSpecParams params; + + if (!nsSMILParserUtils::ParseTimeValueSpecParams(aStringSpec, params)) + return NS_ERROR_FAILURE; + + mParams = params; + + // According to SMIL 3.0: + // The special value "indefinite" does not yield an instance time in the + // begin list. It will, however yield a single instance with the value + // "indefinite" in an end list. This value is not removed by a reset. + if (mParams.mType == nsSMILTimeValueSpecParams::OFFSET || + (!mIsBegin && mParams.mType == nsSMILTimeValueSpecParams::INDEFINITE)) { + mOwner->AddInstanceTime(new nsSMILInstanceTime(mParams.mOffset), mIsBegin); + } + + // Fill in the event symbol to simplify handling later + if (mParams.mType == nsSMILTimeValueSpecParams::REPEAT) { + mParams.mEventSymbol = nsGkAtoms::repeatEvent; + } else if (mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY) { + mParams.mEventSymbol = nsGkAtoms::keypress; + } + + ResolveReferences(aContextNode); + + return NS_OK; +} + +void +nsSMILTimeValueSpec::ResolveReferences(nsIContent* aContextNode) +{ + if (mParams.mType != nsSMILTimeValueSpecParams::SYNCBASE && !IsEventBased()) + return; + + MOZ_ASSERT(aContextNode, + "null context node for resolving timing references against"); + + // If we're not bound to the document yet, don't worry, we'll get called again + // when that happens + if (!aContextNode->IsInUncomposedDoc()) + return; + + // Hold ref to the old element so that it isn't destroyed in between resetting + // the referenced element and using the pointer to update the referenced + // element. + RefPtr<Element> oldReferencedElement = mReferencedElement.get(); + + if (mParams.mDependentElemID) { + mReferencedElement.ResetWithID(aContextNode, + nsDependentAtomString(mParams.mDependentElemID)); + } else if (mParams.mType == nsSMILTimeValueSpecParams::EVENT) { + Element* target = mOwner->GetTargetElement(); + mReferencedElement.ResetWithElement(target); + } else if (mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY) { + nsIDocument* doc = aContextNode->GetUncomposedDoc(); + MOZ_ASSERT(doc, "We are in the document but current doc is null"); + mReferencedElement.ResetWithElement(doc->GetRootElement()); + } else { + MOZ_ASSERT(false, "Syncbase or repeat spec without ID"); + } + UpdateReferencedElement(oldReferencedElement, mReferencedElement.get()); +} + +bool +nsSMILTimeValueSpec::IsEventBased() const +{ + return mParams.mType == nsSMILTimeValueSpecParams::EVENT || + mParams.mType == nsSMILTimeValueSpecParams::REPEAT || + mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY; +} + +void +nsSMILTimeValueSpec::HandleNewInterval(nsSMILInterval& aInterval, + const nsSMILTimeContainer* aSrcContainer) +{ + const nsSMILInstanceTime& baseInstance = mParams.mSyncBegin + ? *aInterval.Begin() : *aInterval.End(); + nsSMILTimeValue newTime = + ConvertBetweenTimeContainers(baseInstance.Time(), aSrcContainer); + + // Apply offset + if (!ApplyOffset(newTime)) { + NS_WARNING("New time overflows nsSMILTime, ignoring"); + return; + } + + // Create the instance time and register it with the interval + RefPtr<nsSMILInstanceTime> newInstance = + new nsSMILInstanceTime(newTime, nsSMILInstanceTime::SOURCE_SYNCBASE, this, + &aInterval); + mOwner->AddInstanceTime(newInstance, mIsBegin); +} + +void +nsSMILTimeValueSpec::HandleTargetElementChange(Element* aNewTarget) +{ + if (!IsEventBased() || mParams.mDependentElemID) + return; + + mReferencedElement.ResetWithElement(aNewTarget); +} + +void +nsSMILTimeValueSpec::HandleChangedInstanceTime( + const nsSMILInstanceTime& aBaseTime, + const nsSMILTimeContainer* aSrcContainer, + nsSMILInstanceTime& aInstanceTimeToUpdate, + bool aObjectChanged) +{ + // If the instance time is fixed (e.g. because it's being used as the begin + // time of an active or postactive interval) we just ignore the change. + if (aInstanceTimeToUpdate.IsFixedTime()) + return; + + nsSMILTimeValue updatedTime = + ConvertBetweenTimeContainers(aBaseTime.Time(), aSrcContainer); + + // Apply offset + if (!ApplyOffset(updatedTime)) { + NS_WARNING("Updated time overflows nsSMILTime, ignoring"); + return; + } + + // The timed element that owns the instance time does the updating so it can + // re-sort its array of instance times more efficiently + if (aInstanceTimeToUpdate.Time() != updatedTime || aObjectChanged) { + mOwner->UpdateInstanceTime(&aInstanceTimeToUpdate, updatedTime, mIsBegin); + } +} + +void +nsSMILTimeValueSpec::HandleDeletedInstanceTime( + nsSMILInstanceTime &aInstanceTime) +{ + mOwner->RemoveInstanceTime(&aInstanceTime, mIsBegin); +} + +bool +nsSMILTimeValueSpec::DependsOnBegin() const +{ + return mParams.mSyncBegin; +} + +void +nsSMILTimeValueSpec::Traverse(nsCycleCollectionTraversalCallback* aCallback) +{ + mReferencedElement.Traverse(aCallback); +} + +void +nsSMILTimeValueSpec::Unlink() +{ + UnregisterFromReferencedElement(mReferencedElement.get()); + mReferencedElement.Unlink(); +} + +//---------------------------------------------------------------------- +// Implementation helpers + +void +nsSMILTimeValueSpec::UpdateReferencedElement(Element* aFrom, Element* aTo) +{ + if (aFrom == aTo) + return; + + UnregisterFromReferencedElement(aFrom); + + switch (mParams.mType) + { + case nsSMILTimeValueSpecParams::SYNCBASE: + { + nsSMILTimedElement* to = GetTimedElement(aTo); + if (to) { + to->AddDependent(*this); + } + } + break; + + case nsSMILTimeValueSpecParams::EVENT: + case nsSMILTimeValueSpecParams::REPEAT: + case nsSMILTimeValueSpecParams::ACCESSKEY: + RegisterEventListener(aTo); + break; + + default: + // not a referencing-type + break; + } +} + +void +nsSMILTimeValueSpec::UnregisterFromReferencedElement(Element* aElement) +{ + if (!aElement) + return; + + if (mParams.mType == nsSMILTimeValueSpecParams::SYNCBASE) { + nsSMILTimedElement* timedElement = GetTimedElement(aElement); + if (timedElement) { + timedElement->RemoveDependent(*this); + } + mOwner->RemoveInstanceTimesForCreator(this, mIsBegin); + } else if (IsEventBased()) { + UnregisterEventListener(aElement); + } +} + +nsSMILTimedElement* +nsSMILTimeValueSpec::GetTimedElement(Element* aElement) +{ + return aElement && aElement->IsNodeOfType(nsINode::eANIMATION) ? + &static_cast<SVGAnimationElement*>(aElement)->TimedElement() : nullptr; +} + +// Indicates whether we're allowed to register an event-listener +// when scripting is disabled. +bool +nsSMILTimeValueSpec::IsWhitelistedEvent() +{ + // The category of (SMIL-specific) "repeat(n)" events are allowed. + if (mParams.mType == nsSMILTimeValueSpecParams::REPEAT) { + return true; + } + + // A specific list of other SMIL-related events are allowed, too. + if (mParams.mType == nsSMILTimeValueSpecParams::EVENT && + (mParams.mEventSymbol == nsGkAtoms::repeat || + mParams.mEventSymbol == nsGkAtoms::repeatEvent || + mParams.mEventSymbol == nsGkAtoms::beginEvent || + mParams.mEventSymbol == nsGkAtoms::endEvent)) { + return true; + } + + return false; +} + +void +nsSMILTimeValueSpec::RegisterEventListener(Element* aTarget) +{ + MOZ_ASSERT(IsEventBased(), + "Attempting to register event-listener for unexpected " + "nsSMILTimeValueSpec type"); + MOZ_ASSERT(mParams.mEventSymbol, + "Attempting to register event-listener but there is no event " + "name"); + + if (!aTarget) + return; + + // When script is disabled, only allow registration for whitelisted events. + if (!aTarget->GetOwnerDocument()->IsScriptEnabled() && + !IsWhitelistedEvent()) { + return; + } + + if (!mEventListener) { + mEventListener = new EventListener(this); + } + + EventListenerManager* elm = GetEventListenerManager(aTarget); + if (!elm) + return; + + elm->AddEventListenerByType(mEventListener, + nsDependentAtomString(mParams.mEventSymbol), + AllEventsAtSystemGroupBubble()); +} + +void +nsSMILTimeValueSpec::UnregisterEventListener(Element* aTarget) +{ + if (!aTarget || !mEventListener) + return; + + EventListenerManager* elm = GetEventListenerManager(aTarget); + if (!elm) + return; + + elm->RemoveEventListenerByType(mEventListener, + nsDependentAtomString(mParams.mEventSymbol), + AllEventsAtSystemGroupBubble()); +} + +EventListenerManager* +nsSMILTimeValueSpec::GetEventListenerManager(Element* aTarget) +{ + MOZ_ASSERT(aTarget, "null target; can't get EventListenerManager"); + + nsCOMPtr<EventTarget> target; + + if (mParams.mType == nsSMILTimeValueSpecParams::ACCESSKEY) { + nsIDocument* doc = aTarget->GetUncomposedDoc(); + if (!doc) + return nullptr; + nsPIDOMWindowOuter* win = doc->GetWindow(); + if (!win) + return nullptr; + target = do_QueryInterface(win); + } else { + target = aTarget; + } + if (!target) + return nullptr; + + return target->GetOrCreateListenerManager(); +} + +void +nsSMILTimeValueSpec::HandleEvent(nsIDOMEvent* aEvent) +{ + MOZ_ASSERT(mEventListener, "Got event without an event listener"); + MOZ_ASSERT(IsEventBased(), + "Got event for non-event nsSMILTimeValueSpec"); + MOZ_ASSERT(aEvent, "No event supplied"); + + // XXX In the long run we should get the time from the event itself which will + // store the time in global document time which we'll need to convert to our + // time container + nsSMILTimeContainer* container = mOwner->GetTimeContainer(); + if (!container) + return; + + if (!CheckEventDetail(aEvent)) + return; + + nsSMILTime currentTime = container->GetCurrentTime(); + nsSMILTimeValue newTime(currentTime); + if (!ApplyOffset(newTime)) { + NS_WARNING("New time generated from event overflows nsSMILTime, ignoring"); + return; + } + + RefPtr<nsSMILInstanceTime> newInstance = + new nsSMILInstanceTime(newTime, nsSMILInstanceTime::SOURCE_EVENT); + mOwner->AddInstanceTime(newInstance, mIsBegin); +} + +bool +nsSMILTimeValueSpec::CheckEventDetail(nsIDOMEvent *aEvent) +{ + switch (mParams.mType) + { + case nsSMILTimeValueSpecParams::REPEAT: + return CheckRepeatEventDetail(aEvent); + + case nsSMILTimeValueSpecParams::ACCESSKEY: + return CheckAccessKeyEventDetail(aEvent); + + default: + // nothing to check + return true; + } +} + +bool +nsSMILTimeValueSpec::CheckRepeatEventDetail(nsIDOMEvent *aEvent) +{ + nsCOMPtr<nsIDOMTimeEvent> timeEvent = do_QueryInterface(aEvent); + if (!timeEvent) { + NS_WARNING("Received a repeat event that was not a DOMTimeEvent"); + return false; + } + + int32_t detail; + timeEvent->GetDetail(&detail); + return detail > 0 && (uint32_t)detail == mParams.mRepeatIterationOrAccessKey; +} + +bool +nsSMILTimeValueSpec::CheckAccessKeyEventDetail(nsIDOMEvent *aEvent) +{ + nsCOMPtr<nsIDOMKeyEvent> keyEvent = do_QueryInterface(aEvent); + if (!keyEvent) { + NS_WARNING("Received an accesskey event that was not a DOMKeyEvent"); + return false; + } + + // Ignore the key event if any modifier keys are pressed UNLESS we're matching + // on the charCode in which case we ignore the state of the shift and alt keys + // since they might be needed to generate the character in question. + bool isCtrl; + bool isMeta; + keyEvent->GetCtrlKey(&isCtrl); + keyEvent->GetMetaKey(&isMeta); + if (isCtrl || isMeta) + return false; + + uint32_t code; + keyEvent->GetCharCode(&code); + if (code) + return code == mParams.mRepeatIterationOrAccessKey; + + // Only match on the keyCode if it corresponds to some ASCII character that + // does not produce a charCode. + // In this case we can safely bail out if either alt or shift is pressed since + // they won't already be incorporated into the keyCode unlike the charCode. + bool isAlt; + bool isShift; + keyEvent->GetAltKey(&isAlt); + keyEvent->GetShiftKey(&isShift); + if (isAlt || isShift) + return false; + + keyEvent->GetKeyCode(&code); + switch (code) + { + case nsIDOMKeyEvent::DOM_VK_BACK_SPACE: + return mParams.mRepeatIterationOrAccessKey == 0x08; + + case nsIDOMKeyEvent::DOM_VK_RETURN: + return mParams.mRepeatIterationOrAccessKey == 0x0A || + mParams.mRepeatIterationOrAccessKey == 0x0D; + + case nsIDOMKeyEvent::DOM_VK_ESCAPE: + return mParams.mRepeatIterationOrAccessKey == 0x1B; + + case nsIDOMKeyEvent::DOM_VK_DELETE: + return mParams.mRepeatIterationOrAccessKey == 0x7F; + + default: + return false; + } +} + +nsSMILTimeValue +nsSMILTimeValueSpec::ConvertBetweenTimeContainers( + const nsSMILTimeValue& aSrcTime, + const nsSMILTimeContainer* aSrcContainer) +{ + // If the source time is either indefinite or unresolved the result is going + // to be the same + if (!aSrcTime.IsDefinite()) + return aSrcTime; + + // Convert from source time container to our parent time container + const nsSMILTimeContainer* dstContainer = mOwner->GetTimeContainer(); + if (dstContainer == aSrcContainer) + return aSrcTime; + + // If one of the elements is not attached to a time container then we can't do + // any meaningful conversion + if (!aSrcContainer || !dstContainer) + return nsSMILTimeValue(); // unresolved + + nsSMILTimeValue docTime = + aSrcContainer->ContainerToParentTime(aSrcTime.GetMillis()); + + if (docTime.IsIndefinite()) + // This will happen if the source container is paused and we have a future + // time. Just return the indefinite time. + return docTime; + + MOZ_ASSERT(docTime.IsDefinite(), + "ContainerToParentTime gave us an unresolved or indefinite time"); + + return dstContainer->ParentToContainerTime(docTime.GetMillis()); +} + +bool +nsSMILTimeValueSpec::ApplyOffset(nsSMILTimeValue& aTime) const +{ + // indefinite + offset = indefinite. Likewise for unresolved times. + if (!aTime.IsDefinite()) { + return true; + } + + double resultAsDouble = + (double)aTime.GetMillis() + mParams.mOffset.GetMillis(); + if (resultAsDouble > std::numeric_limits<nsSMILTime>::max() || + resultAsDouble < std::numeric_limits<nsSMILTime>::min()) { + return false; + } + aTime.SetMillis(aTime.GetMillis() + mParams.mOffset.GetMillis()); + return true; +} diff --git a/dom/smil/nsSMILTimeValueSpec.h b/dom/smil/nsSMILTimeValueSpec.h new file mode 100644 index 000000000..d0817c15f --- /dev/null +++ b/dom/smil/nsSMILTimeValueSpec.h @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILTIMEVALUESPEC_H_ +#define NS_SMILTIMEVALUESPEC_H_ + +#include "mozilla/Attributes.h" +#include "nsSMILTimeValueSpecParams.h" +#include "nsReferencedElement.h" +#include "nsIDOMEventListener.h" + +class nsAString; +class nsSMILTimeValue; +class nsSMILTimedElement; +class nsSMILTimeContainer; +class nsSMILInstanceTime; +class nsSMILInterval; + +namespace mozilla { +class EventListenerManager; +} // namespace mozilla + +//---------------------------------------------------------------------- +// nsSMILTimeValueSpec class +// +// An individual element of a 'begin' or 'end' attribute, e.g. '5s', 'a.end'. +// This class handles the parsing of such specifications and performs the +// necessary event handling (for event, repeat, and accesskey specifications) +// and synchronisation (for syncbase specifications). +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in nsSMILTimeValue.h + +class nsSMILTimeValueSpec +{ +public: + typedef mozilla::dom::Element Element; + + nsSMILTimeValueSpec(nsSMILTimedElement& aOwner, bool aIsBegin); + ~nsSMILTimeValueSpec(); + + nsresult SetSpec(const nsAString& aStringSpec, Element* aContextNode); + void ResolveReferences(nsIContent* aContextNode); + bool IsEventBased() const; + + void HandleNewInterval(nsSMILInterval& aInterval, + const nsSMILTimeContainer* aSrcContainer); + void HandleTargetElementChange(Element* aNewTarget); + + // For created nsSMILInstanceTime objects + bool DependsOnBegin() const; + void HandleChangedInstanceTime(const nsSMILInstanceTime& aBaseTime, + const nsSMILTimeContainer* aSrcContainer, + nsSMILInstanceTime& aInstanceTimeToUpdate, + bool aObjectChanged); + void HandleDeletedInstanceTime(nsSMILInstanceTime& aInstanceTime); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + +protected: + void UpdateReferencedElement(Element* aFrom, Element* aTo); + void UnregisterFromReferencedElement(Element* aElement); + nsSMILTimedElement* GetTimedElement(Element* aElement); + bool IsWhitelistedEvent(); + void RegisterEventListener(Element* aElement); + void UnregisterEventListener(Element* aElement); + mozilla::EventListenerManager* GetEventListenerManager(Element* aElement); + void HandleEvent(nsIDOMEvent* aEvent); + bool CheckEventDetail(nsIDOMEvent* aEvent); + bool CheckRepeatEventDetail(nsIDOMEvent* aEvent); + bool CheckAccessKeyEventDetail(nsIDOMEvent* aEvent); + nsSMILTimeValue ConvertBetweenTimeContainers(const nsSMILTimeValue& aSrcTime, + const nsSMILTimeContainer* aSrcContainer); + bool ApplyOffset(nsSMILTimeValue& aTime) const; + + nsSMILTimedElement* mOwner; + bool mIsBegin; // Indicates if *we* are a begin spec, + // not to be confused with + // mParams.mSyncBegin which indicates + // if we're synced with the begin of + // the target. + nsSMILTimeValueSpecParams mParams; + + class TimeReferenceElement : public nsReferencedElement + { + public: + explicit TimeReferenceElement(nsSMILTimeValueSpec* aOwner) : mSpec(aOwner) { } + void ResetWithElement(Element* aTo) { + RefPtr<Element> from = get(); + Unlink(); + ElementChanged(from, aTo); + } + + protected: + virtual void ElementChanged(Element* aFrom, Element* aTo) override + { + nsReferencedElement::ElementChanged(aFrom, aTo); + mSpec->UpdateReferencedElement(aFrom, aTo); + } + virtual bool IsPersistent() override { return true; } + private: + nsSMILTimeValueSpec* mSpec; + }; + + TimeReferenceElement mReferencedElement; + + class EventListener final : public nsIDOMEventListener + { + ~EventListener() {} + public: + explicit EventListener(nsSMILTimeValueSpec* aOwner) : mSpec(aOwner) { } + void Disconnect() + { + mSpec = nullptr; + } + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + private: + nsSMILTimeValueSpec* mSpec; + }; + RefPtr<EventListener> mEventListener; +}; + +#endif // NS_SMILTIMEVALUESPEC_H_ diff --git a/dom/smil/nsSMILTimeValueSpecParams.h b/dom/smil/nsSMILTimeValueSpecParams.h new file mode 100644 index 000000000..7a5ada626 --- /dev/null +++ b/dom/smil/nsSMILTimeValueSpecParams.h @@ -0,0 +1,68 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILTIMEVALUESPECPARAMS_H_ +#define NS_SMILTIMEVALUESPECPARAMS_H_ + +#include "nsSMILTimeValue.h" +#include "nsIAtom.h" + +//---------------------------------------------------------------------- +// nsSMILTimeValueSpecParams +// +// A simple data type for storing the result of parsing a single begin or end +// value (e.g. the '5s' in begin="5s; indefinite; a.begin+2s"). + +class nsSMILTimeValueSpecParams +{ +public: + nsSMILTimeValueSpecParams() + : + mType(INDEFINITE), + mSyncBegin(false), + mRepeatIterationOrAccessKey(0) + { } + + // The type of value this specification describes + enum { + OFFSET, + SYNCBASE, + EVENT, + REPEAT, + ACCESSKEY, + WALLCLOCK, + INDEFINITE + } mType; + + // A clock value that is added to: + // - type OFFSET: the document begin + // - type SYNCBASE: the timebase's begin or end time + // - type EVENT: the event time + // - type REPEAT: the repeat time + // - type ACCESSKEY: the keypress time + // It is not used for WALLCLOCK or INDEFINITE times + nsSMILTimeValue mOffset; + + // The base element that this specification refers to. + // For SYNCBASE types, this is the timebase + // For EVENT and REPEAT types, this is the eventbase + RefPtr<nsIAtom> mDependentElemID; + + // The event to respond to. + // Only used for EVENT types. + RefPtr<nsIAtom> mEventSymbol; + + // Indicates if this specification refers to the begin or end of the dependent + // element. + // Only used for SYNCBASE types. + bool mSyncBegin; + + // The repeat iteration (type=REPEAT) or access key (type=ACCESSKEY) to + // respond to. + uint32_t mRepeatIterationOrAccessKey; +}; + +#endif // NS_SMILTIMEVALUESPECPARAMS_H_ diff --git a/dom/smil/nsSMILTimedElement.cpp b/dom/smil/nsSMILTimedElement.cpp new file mode 100644 index 000000000..120536be0 --- /dev/null +++ b/dom/smil/nsSMILTimedElement.cpp @@ -0,0 +1,2444 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/DebugOnly.h" + +#include "mozilla/ContentEvents.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsAutoPtr.h" +#include "nsSMILTimedElement.h" +#include "nsAttrValueInlines.h" +#include "nsSMILAnimationFunction.h" +#include "nsSMILTimeValue.h" +#include "nsSMILTimeValueSpec.h" +#include "nsSMILInstanceTime.h" +#include "nsSMILParserUtils.h" +#include "nsSMILTimeContainer.h" +#include "nsGkAtoms.h" +#include "nsReadableUtils.h" +#include "nsMathUtils.h" +#include "nsThreadUtils.h" +#include "nsIPresShell.h" +#include "prdtoa.h" +#include "plstr.h" +#include "prtime.h" +#include "nsString.h" +#include "mozilla/AutoRestore.h" +#include "nsCharSeparatedTokenizer.h" +#include <algorithm> + +using namespace mozilla; +using namespace mozilla::dom; + +//---------------------------------------------------------------------- +// Helper class: InstanceTimeComparator + +// Upon inserting an instance time into one of our instance time lists we assign +// it a serial number. This allows us to sort the instance times in such a way +// that where we have several equal instance times, the ones added later will +// sort later. This means that when we call UpdateCurrentInterval during the +// waiting state we won't unnecessarily change the begin instance. +// +// The serial number also means that every instance time has an unambiguous +// position in the array so we can use RemoveElementSorted and the like. +bool +nsSMILTimedElement::InstanceTimeComparator::Equals( + const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const +{ + MOZ_ASSERT(aElem1 && aElem2, + "Trying to compare null instance time pointers"); + MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(), + "Instance times have not been assigned serial numbers"); + MOZ_ASSERT(aElem1 == aElem2 || aElem1->Serial() != aElem2->Serial(), + "Serial numbers are not unique"); + + return aElem1->Serial() == aElem2->Serial(); +} + +bool +nsSMILTimedElement::InstanceTimeComparator::LessThan( + const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const +{ + MOZ_ASSERT(aElem1 && aElem2, + "Trying to compare null instance time pointers"); + MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(), + "Instance times have not been assigned serial numbers"); + + int8_t cmp = aElem1->Time().CompareTo(aElem2->Time()); + return cmp == 0 ? aElem1->Serial() < aElem2->Serial() : cmp < 0; +} + +//---------------------------------------------------------------------- +// Helper class: AsyncTimeEventRunner + +namespace +{ + class AsyncTimeEventRunner : public Runnable + { + protected: + RefPtr<nsIContent> mTarget; + EventMessage mMsg; + int32_t mDetail; + + public: + AsyncTimeEventRunner(nsIContent* aTarget, EventMessage aMsg, + int32_t aDetail) + : mTarget(aTarget) + , mMsg(aMsg) + , mDetail(aDetail) + { + } + + NS_IMETHOD Run() override + { + InternalSMILTimeEvent event(true, mMsg); + event.mDetail = mDetail; + + nsPresContext* context = nullptr; + nsIDocument* doc = mTarget->GetUncomposedDoc(); + if (doc) { + nsCOMPtr<nsIPresShell> shell = doc->GetShell(); + if (shell) { + context = shell->GetPresContext(); + } + } + + return EventDispatcher::Dispatch(mTarget, context, &event); + } + }; +} // namespace + +//---------------------------------------------------------------------- +// Helper class: AutoIntervalUpdateBatcher + +// Stack-based helper class to set the mDeferIntervalUpdates flag on an +// nsSMILTimedElement and perform the UpdateCurrentInterval when the object is +// destroyed. +// +// If several of these objects are allocated on the stack, the update will not +// be performed until the last object for a given nsSMILTimedElement is +// destroyed. +class MOZ_STACK_CLASS nsSMILTimedElement::AutoIntervalUpdateBatcher +{ +public: + explicit AutoIntervalUpdateBatcher(nsSMILTimedElement& aTimedElement) + : mTimedElement(aTimedElement), + mDidSetFlag(!aTimedElement.mDeferIntervalUpdates) + { + mTimedElement.mDeferIntervalUpdates = true; + } + + ~AutoIntervalUpdateBatcher() + { + if (!mDidSetFlag) + return; + + mTimedElement.mDeferIntervalUpdates = false; + + if (mTimedElement.mDoDeferredUpdate) { + mTimedElement.mDoDeferredUpdate = false; + mTimedElement.UpdateCurrentInterval(); + } + } + +private: + nsSMILTimedElement& mTimedElement; + bool mDidSetFlag; +}; + +//---------------------------------------------------------------------- +// Helper class: AutoIntervalUpdater + +// Stack-based helper class to call UpdateCurrentInterval when it is destroyed +// which helps avoid bugs where we forget to call UpdateCurrentInterval in the +// case of early returns (e.g. due to parse errors). +// +// This can be safely used in conjunction with AutoIntervalUpdateBatcher; any +// calls to UpdateCurrentInterval made by this class will simply be deferred if +// there is an AutoIntervalUpdateBatcher on the stack. +class MOZ_STACK_CLASS nsSMILTimedElement::AutoIntervalUpdater +{ +public: + explicit AutoIntervalUpdater(nsSMILTimedElement& aTimedElement) + : mTimedElement(aTimedElement) { } + + ~AutoIntervalUpdater() + { + mTimedElement.UpdateCurrentInterval(); + } + +private: + nsSMILTimedElement& mTimedElement; +}; + +//---------------------------------------------------------------------- +// Templated helper functions + +// Selectively remove elements from an array of type +// nsTArray<RefPtr<nsSMILInstanceTime> > with O(n) performance. +template <class TestFunctor> +void +nsSMILTimedElement::RemoveInstanceTimes(InstanceTimeList& aArray, + TestFunctor& aTest) +{ + InstanceTimeList newArray; + for (uint32_t i = 0; i < aArray.Length(); ++i) { + nsSMILInstanceTime* item = aArray[i].get(); + if (aTest(item, i)) { + // As per bugs 665334 and 669225 we should be careful not to remove the + // instance time that corresponds to the previous interval's end time. + // + // Most functors supplied here fulfil this condition by checking if the + // instance time is marked as "ShouldPreserve" and if so, not deleting it. + // + // However, when filtering instance times, we sometimes need to drop even + // instance times marked as "ShouldPreserve". In that case we take special + // care not to delete the end instance time of the previous interval. + MOZ_ASSERT(!GetPreviousInterval() || item != GetPreviousInterval()->End(), + "Removing end instance time of previous interval"); + item->Unlink(); + } else { + newArray.AppendElement(item); + } + } + aArray.Clear(); + aArray.SwapElements(newArray); +} + +//---------------------------------------------------------------------- +// Static members + +nsAttrValue::EnumTable nsSMILTimedElement::sFillModeTable[] = { + {"remove", FILL_REMOVE}, + {"freeze", FILL_FREEZE}, + {nullptr, 0} +}; + +nsAttrValue::EnumTable nsSMILTimedElement::sRestartModeTable[] = { + {"always", RESTART_ALWAYS}, + {"whenNotActive", RESTART_WHENNOTACTIVE}, + {"never", RESTART_NEVER}, + {nullptr, 0} +}; + +const nsSMILMilestone nsSMILTimedElement::sMaxMilestone(INT64_MAX, false); + +// The thresholds at which point we start filtering intervals and instance times +// indiscriminately. +// See FilterIntervals and FilterInstanceTimes. +const uint8_t nsSMILTimedElement::sMaxNumIntervals = 20; +const uint8_t nsSMILTimedElement::sMaxNumInstanceTimes = 100; + +// Detect if we arrive in some sort of undetected recursive syncbase dependency +// relationship +const uint8_t nsSMILTimedElement::sMaxUpdateIntervalRecursionDepth = 20; + +//---------------------------------------------------------------------- +// Ctor, dtor + +nsSMILTimedElement::nsSMILTimedElement() +: + mAnimationElement(nullptr), + mFillMode(FILL_REMOVE), + mRestartMode(RESTART_ALWAYS), + mInstanceSerialIndex(0), + mClient(nullptr), + mCurrentInterval(nullptr), + mCurrentRepeatIteration(0), + mPrevRegisteredMilestone(sMaxMilestone), + mElementState(STATE_STARTUP), + mSeekState(SEEK_NOT_SEEKING), + mDeferIntervalUpdates(false), + mDoDeferredUpdate(false), + mIsDisabled(false), + mDeleteCount(0), + mUpdateIntervalRecursionDepth(0) +{ + mSimpleDur.SetIndefinite(); + mMin.SetMillis(0L); + mMax.SetIndefinite(); +} + +nsSMILTimedElement::~nsSMILTimedElement() +{ + // Unlink all instance times from dependent intervals + for (uint32_t i = 0; i < mBeginInstances.Length(); ++i) { + mBeginInstances[i]->Unlink(); + } + mBeginInstances.Clear(); + for (uint32_t i = 0; i < mEndInstances.Length(); ++i) { + mEndInstances[i]->Unlink(); + } + mEndInstances.Clear(); + + // Notify anyone listening to our intervals that they're gone + // (We shouldn't get any callbacks from this because all our instance times + // are now disassociated with any intervals) + ClearIntervals(); + + // The following assertions are important in their own right (for checking + // correct behavior) but also because AutoIntervalUpdateBatcher holds pointers + // to class so if they fail there's the possibility we might have dangling + // pointers. + MOZ_ASSERT(!mDeferIntervalUpdates, + "Interval updates should no longer be blocked when an " + "nsSMILTimedElement disappears"); + MOZ_ASSERT(!mDoDeferredUpdate, + "There should no longer be any pending updates when an " + "nsSMILTimedElement disappears"); +} + +void +nsSMILTimedElement::SetAnimationElement(SVGAnimationElement* aElement) +{ + MOZ_ASSERT(aElement, "NULL owner element"); + MOZ_ASSERT(!mAnimationElement, "Re-setting owner"); + mAnimationElement = aElement; +} + +nsSMILTimeContainer* +nsSMILTimedElement::GetTimeContainer() +{ + return mAnimationElement ? mAnimationElement->GetTimeContainer() : nullptr; +} + +dom::Element* +nsSMILTimedElement::GetTargetElement() +{ + return mAnimationElement ? + mAnimationElement->GetTargetElementContent() : + nullptr; +} + +//---------------------------------------------------------------------- +// nsIDOMElementTimeControl methods +// +// The definition of the ElementTimeControl interface differs between SMIL +// Animation and SVG 1.1. In SMIL Animation all methods have a void return +// type and the new instance time is simply added to the list and restart +// semantics are applied as with any other instance time. In the SVG definition +// the methods return a bool depending on the restart mode. +// +// This inconsistency has now been addressed by an erratum in SVG 1.1: +// +// http://www.w3.org/2003/01/REC-SVG11-20030114-errata#elementtimecontrol-interface +// +// which favours the definition in SMIL, i.e. instance times are just added +// without first checking the restart mode. + +nsresult +nsSMILTimedElement::BeginElementAt(double aOffsetSeconds) +{ + nsSMILTimeContainer* container = GetTimeContainer(); + if (!container) + return NS_ERROR_FAILURE; + + nsSMILTime currentTime = container->GetCurrentTime(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, true); +} + +nsresult +nsSMILTimedElement::EndElementAt(double aOffsetSeconds) +{ + nsSMILTimeContainer* container = GetTimeContainer(); + if (!container) + return NS_ERROR_FAILURE; + + nsSMILTime currentTime = container->GetCurrentTime(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, false); +} + +//---------------------------------------------------------------------- +// nsSVGAnimationElement methods + +nsSMILTimeValue +nsSMILTimedElement::GetStartTime() const +{ + return mElementState == STATE_WAITING || mElementState == STATE_ACTIVE + ? mCurrentInterval->Begin()->Time() + : nsSMILTimeValue(); +} + +//---------------------------------------------------------------------- +// Hyperlinking support + +nsSMILTimeValue +nsSMILTimedElement::GetHyperlinkTime() const +{ + nsSMILTimeValue hyperlinkTime; // Default ctor creates unresolved time + + if (mElementState == STATE_ACTIVE) { + hyperlinkTime = mCurrentInterval->Begin()->Time(); + } else if (!mBeginInstances.IsEmpty()) { + hyperlinkTime = mBeginInstances[0]->Time(); + } + + return hyperlinkTime; +} + +//---------------------------------------------------------------------- +// nsSMILTimedElement + +void +nsSMILTimedElement::AddInstanceTime(nsSMILInstanceTime* aInstanceTime, + bool aIsBegin) +{ + MOZ_ASSERT(aInstanceTime, "Attempting to add null instance time"); + + // Event-sensitivity: If an element is not active (but the parent time + // container is), then events are only handled for begin specifications. + if (mElementState != STATE_ACTIVE && !aIsBegin && + aInstanceTime->IsDynamic()) + { + // No need to call Unlink here--dynamic instance times shouldn't be linked + // to anything that's going to miss them + MOZ_ASSERT(!aInstanceTime->GetBaseInterval(), + "Dynamic instance time has a base interval--we probably need " + "to unlink it if we're not going to use it"); + return; + } + + aInstanceTime->SetSerial(++mInstanceSerialIndex); + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + RefPtr<nsSMILInstanceTime>* inserted = + instanceList.InsertElementSorted(aInstanceTime, InstanceTimeComparator()); + if (!inserted) { + NS_WARNING("Insufficient memory to insert instance time"); + return; + } + + UpdateCurrentInterval(); +} + +void +nsSMILTimedElement::UpdateInstanceTime(nsSMILInstanceTime* aInstanceTime, + nsSMILTimeValue& aUpdatedTime, + bool aIsBegin) +{ + MOZ_ASSERT(aInstanceTime, "Attempting to update null instance time"); + + // The reason we update the time here and not in the nsSMILTimeValueSpec is + // that it means we *could* re-sort more efficiently by doing a sorted remove + // and insert but currently this doesn't seem to be necessary given how + // infrequently we get these change notices. + aInstanceTime->DependentUpdate(aUpdatedTime); + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + instanceList.Sort(InstanceTimeComparator()); + + // Generally speaking, UpdateCurrentInterval makes changes to the current + // interval and sends changes notices itself. However, in this case because + // instance times are shared between the instance time list and the intervals + // we are effectively changing the current interval outside + // UpdateCurrentInterval so we need to explicitly signal that we've made + // a change. + // + // This wouldn't be necessary if we cloned instance times on adding them to + // the current interval but this introduces other complications (particularly + // detecting which instance time is being used to define the begin of the + // current interval when doing a Reset). + bool changedCurrentInterval = mCurrentInterval && + (mCurrentInterval->Begin() == aInstanceTime || + mCurrentInterval->End() == aInstanceTime); + + UpdateCurrentInterval(changedCurrentInterval); +} + +void +nsSMILTimedElement::RemoveInstanceTime(nsSMILInstanceTime* aInstanceTime, + bool aIsBegin) +{ + MOZ_ASSERT(aInstanceTime, "Attempting to remove null instance time"); + + // If the instance time should be kept (because it is or was the fixed end + // point of an interval) then just disassociate it from the creator. + if (aInstanceTime->ShouldPreserve()) { + aInstanceTime->Unlink(); + return; + } + + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + mozilla::DebugOnly<bool> found = + instanceList.RemoveElementSorted(aInstanceTime, InstanceTimeComparator()); + MOZ_ASSERT(found, "Couldn't find instance time to delete"); + + UpdateCurrentInterval(); +} + +namespace +{ + class MOZ_STACK_CLASS RemoveByCreator + { + public: + explicit RemoveByCreator(const nsSMILTimeValueSpec* aCreator) : mCreator(aCreator) + { } + + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + if (aInstanceTime->GetCreator() != mCreator) + return false; + + // If the instance time should be kept (because it is or was the fixed end + // point of an interval) then just disassociate it from the creator. + if (aInstanceTime->ShouldPreserve()) { + aInstanceTime->Unlink(); + return false; + } + + return true; + } + + private: + const nsSMILTimeValueSpec* mCreator; + }; +} // namespace + +void +nsSMILTimedElement::RemoveInstanceTimesForCreator( + const nsSMILTimeValueSpec* aCreator, bool aIsBegin) +{ + MOZ_ASSERT(aCreator, "Creator not set"); + + InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances; + RemoveByCreator removeByCreator(aCreator); + RemoveInstanceTimes(instances, removeByCreator); + + UpdateCurrentInterval(); +} + +void +nsSMILTimedElement::SetTimeClient(nsSMILAnimationFunction* aClient) +{ + // + // No need to check for nullptr. A nullptr parameter simply means to remove the + // previous client which we do by setting to nullptr anyway. + // + + mClient = aClient; +} + +void +nsSMILTimedElement::SampleAt(nsSMILTime aContainerTime) +{ + if (mIsDisabled) + return; + + // Milestones are cleared before a sample + mPrevRegisteredMilestone = sMaxMilestone; + + DoSampleAt(aContainerTime, false); +} + +void +nsSMILTimedElement::SampleEndAt(nsSMILTime aContainerTime) +{ + if (mIsDisabled) + return; + + // Milestones are cleared before a sample + mPrevRegisteredMilestone = sMaxMilestone; + + // If the current interval changes, we don't bother trying to remove any old + // milestones we'd registered. So it's possible to get a call here to end an + // interval at a time that no longer reflects the end of the current interval. + // + // For now we just check that we're actually in an interval but note that the + // initial sample we use to initialise the model is an end sample. This is + // because we want to resolve all the instance times before committing to an + // initial interval. Therefore an end sample from the startup state is also + // acceptable. + if (mElementState == STATE_ACTIVE || mElementState == STATE_STARTUP) { + DoSampleAt(aContainerTime, true); // End sample + } else { + // Even if this was an unnecessary milestone sample we want to be sure that + // our next real milestone is registered. + RegisterMilestone(); + } +} + +void +nsSMILTimedElement::DoSampleAt(nsSMILTime aContainerTime, bool aEndOnly) +{ + MOZ_ASSERT(mAnimationElement, + "Got sample before being registered with an animation element"); + MOZ_ASSERT(GetTimeContainer(), + "Got sample without being registered with a time container"); + + // This could probably happen if we later implement externalResourcesRequired + // (bug 277955) and whilst waiting for those resources (and the animation to + // start) we transfer a node from another document fragment that has already + // started. In such a case we might receive milestone samples registered with + // the already active container. + if (GetTimeContainer()->IsPausedByType(nsSMILTimeContainer::PAUSE_BEGIN)) + return; + + // We use an end-sample to start animation since an end-sample lets us + // tentatively create an interval without committing to it (by transitioning + // to the ACTIVE state) and this is necessary because we might have + // dependencies on other animations that are yet to start. After these + // other animations start, it may be necessary to revise our initial interval. + // + // However, sometimes instead of an end-sample we can get a regular sample + // during STARTUP state. This can happen, for example, if we register + // a milestone before time t=0 and are then re-bound to the tree (which sends + // us back to the STARTUP state). In such a case we should just ignore the + // sample and wait for our real initial sample which will be an end-sample. + if (mElementState == STATE_STARTUP && !aEndOnly) + return; + + bool finishedSeek = false; + if (GetTimeContainer()->IsSeeking() && mSeekState == SEEK_NOT_SEEKING) { + mSeekState = mElementState == STATE_ACTIVE ? + SEEK_FORWARD_FROM_ACTIVE : + SEEK_FORWARD_FROM_INACTIVE; + } else if (mSeekState != SEEK_NOT_SEEKING && + !GetTimeContainer()->IsSeeking()) { + finishedSeek = true; + } + + bool stateChanged; + nsSMILTimeValue sampleTime(aContainerTime); + + do { +#ifdef DEBUG + // Check invariant + if (mElementState == STATE_STARTUP || mElementState == STATE_POSTACTIVE) { + MOZ_ASSERT(!mCurrentInterval, + "Shouldn't have current interval in startup or postactive " + "states"); + } else { + MOZ_ASSERT(mCurrentInterval, + "Should have current interval in waiting and active states"); + } +#endif + + stateChanged = false; + + switch (mElementState) + { + case STATE_STARTUP: + { + nsSMILInterval firstInterval; + mElementState = GetNextInterval(nullptr, nullptr, nullptr, firstInterval) + ? STATE_WAITING + : STATE_POSTACTIVE; + stateChanged = true; + if (mElementState == STATE_WAITING) { + mCurrentInterval = new nsSMILInterval(firstInterval); + NotifyNewInterval(); + } + } + break; + + case STATE_WAITING: + { + if (mCurrentInterval->Begin()->Time() <= sampleTime) { + mElementState = STATE_ACTIVE; + mCurrentInterval->FixBegin(); + if (mClient) { + mClient->Activate(mCurrentInterval->Begin()->Time().GetMillis()); + } + if (mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILBeginEvent, 0); + } + if (HasPlayed()) { + Reset(); // Apply restart behaviour + // The call to Reset() may mean that the end point of our current + // interval should be changed and so we should update the interval + // now. However, calling UpdateCurrentInterval could result in the + // interval getting deleted (perhaps through some web of syncbase + // dependencies) therefore we make updating the interval the last + // thing we do. There is no guarantee that mCurrentInterval is set + // after this. + UpdateCurrentInterval(); + } + stateChanged = true; + } + } + break; + + case STATE_ACTIVE: + { + // Ending early will change the interval but we don't notify dependents + // of the change until we have closed off the current interval (since we + // don't want dependencies to un-end our early end). + bool didApplyEarlyEnd = ApplyEarlyEnd(sampleTime); + + if (mCurrentInterval->End()->Time() <= sampleTime) { + nsSMILInterval newInterval; + mElementState = + GetNextInterval(mCurrentInterval, nullptr, nullptr, newInterval) + ? STATE_WAITING + : STATE_POSTACTIVE; + if (mClient) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + } + mCurrentInterval->FixEnd(); + if (mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILEndEvent, 0); + } + mCurrentRepeatIteration = 0; + mOldIntervals.AppendElement(mCurrentInterval.forget()); + SampleFillValue(); + if (mElementState == STATE_WAITING) { + mCurrentInterval = new nsSMILInterval(newInterval); + } + // We are now in a consistent state to dispatch notifications + if (didApplyEarlyEnd) { + NotifyChangedInterval( + mOldIntervals[mOldIntervals.Length() - 1], false, true); + } + if (mElementState == STATE_WAITING) { + NotifyNewInterval(); + } + FilterHistory(); + stateChanged = true; + } else { + MOZ_ASSERT(!didApplyEarlyEnd, + "We got an early end, but didn't end"); + nsSMILTime beginTime = mCurrentInterval->Begin()->Time().GetMillis(); + NS_ASSERTION(aContainerTime >= beginTime, + "Sample time should not precede current interval"); + nsSMILTime activeTime = aContainerTime - beginTime; + + // The 'min' attribute can cause the active interval to be longer than + // the 'repeating interval'. + // In that extended period we apply the fill mode. + if (GetRepeatDuration() <= nsSMILTimeValue(activeTime)) { + if (mClient && mClient->IsActive()) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + } + SampleFillValue(); + } else { + SampleSimpleTime(activeTime); + + // We register our repeat times as milestones (except when we're + // seeking) so we should get a sample at exactly the time we repeat. + // (And even when we are seeking we want to update + // mCurrentRepeatIteration so we do that first before testing the + // seek state.) + uint32_t prevRepeatIteration = mCurrentRepeatIteration; + if ( + ActiveTimeToSimpleTime(activeTime, mCurrentRepeatIteration)==0 && + mCurrentRepeatIteration != prevRepeatIteration && + mCurrentRepeatIteration && + mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILRepeatEvent, + static_cast<int32_t>(mCurrentRepeatIteration)); + } + } + } + } + break; + + case STATE_POSTACTIVE: + break; + } + + // Generally we continue driving the state machine so long as we have changed + // state. However, for end samples we only drive the state machine as far as + // the waiting or postactive state because we don't want to commit to any new + // interval (by transitioning to the active state) until all the end samples + // have finished and we then have complete information about the available + // instance times upon which to base our next interval. + } while (stateChanged && (!aEndOnly || (mElementState != STATE_WAITING && + mElementState != STATE_POSTACTIVE))); + + if (finishedSeek) { + DoPostSeek(); + } + RegisterMilestone(); +} + +void +nsSMILTimedElement::HandleContainerTimeChange() +{ + // In future we could possibly introduce a separate change notice for time + // container changes and only notify those dependents who live in other time + // containers. For now we don't bother because when we re-resolve the time in + // the nsSMILTimeValueSpec we'll check if anything has changed and if not, we + // won't go any further. + if (mElementState == STATE_WAITING || mElementState == STATE_ACTIVE) { + NotifyChangedInterval(mCurrentInterval, false, false); + } +} + +namespace +{ + bool + RemoveNonDynamic(nsSMILInstanceTime* aInstanceTime) + { + // Generally dynamically-generated instance times (DOM calls, event-based + // times) are not associated with their creator nsSMILTimeValueSpec since + // they may outlive them. + MOZ_ASSERT(!aInstanceTime->IsDynamic() || !aInstanceTime->GetCreator(), + "Dynamic instance time should be unlinked from its creator"); + return !aInstanceTime->IsDynamic() && !aInstanceTime->ShouldPreserve(); + } +} // namespace + +void +nsSMILTimedElement::Rewind() +{ + MOZ_ASSERT(mAnimationElement, + "Got rewind request before being attached to an animation " + "element"); + + // It's possible to get a rewind request whilst we're already in the middle of + // a backwards seek. This can happen when we're performing tree surgery and + // seeking containers at the same time because we can end up requesting + // a local rewind on an element after binding it to a new container and then + // performing a rewind on that container as a whole without sampling in + // between. + // + // However, it should currently be impossible to get a rewind in the middle of + // a forwards seek since forwards seeks are detected and processed within the + // same (re)sample. + if (mSeekState == SEEK_NOT_SEEKING) { + mSeekState = mElementState == STATE_ACTIVE ? + SEEK_BACKWARD_FROM_ACTIVE : + SEEK_BACKWARD_FROM_INACTIVE; + } + MOZ_ASSERT(mSeekState == SEEK_BACKWARD_FROM_INACTIVE || + mSeekState == SEEK_BACKWARD_FROM_ACTIVE, + "Rewind in the middle of a forwards seek?"); + + ClearTimingState(RemoveNonDynamic); + RebuildTimingState(RemoveNonDynamic); + + MOZ_ASSERT(!mCurrentInterval, + "Current interval is set at end of rewind"); +} + +namespace +{ + bool + RemoveAll(nsSMILInstanceTime* aInstanceTime) + { + return true; + } +} // namespace + +bool +nsSMILTimedElement::SetIsDisabled(bool aIsDisabled) +{ + if (mIsDisabled == aIsDisabled) + return false; + + if (aIsDisabled) { + mIsDisabled = true; + ClearTimingState(RemoveAll); + } else { + RebuildTimingState(RemoveAll); + mIsDisabled = false; + } + return true; +} + +namespace +{ + bool + RemoveNonDOM(nsSMILInstanceTime* aInstanceTime) + { + return !aInstanceTime->FromDOM() && !aInstanceTime->ShouldPreserve(); + } +} // namespace + +bool +nsSMILTimedElement::SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, + Element* aContextNode, + nsresult* aParseResult) +{ + bool foundMatch = true; + nsresult parseResult = NS_OK; + + if (aAttribute == nsGkAtoms::begin) { + parseResult = SetBeginSpec(aValue, aContextNode, RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::dur) { + parseResult = SetSimpleDuration(aValue); + } else if (aAttribute == nsGkAtoms::end) { + parseResult = SetEndSpec(aValue, aContextNode, RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::fill) { + parseResult = SetFillMode(aValue); + } else if (aAttribute == nsGkAtoms::max) { + parseResult = SetMax(aValue); + } else if (aAttribute == nsGkAtoms::min) { + parseResult = SetMin(aValue); + } else if (aAttribute == nsGkAtoms::repeatCount) { + parseResult = SetRepeatCount(aValue); + } else if (aAttribute == nsGkAtoms::repeatDur) { + parseResult = SetRepeatDur(aValue); + } else if (aAttribute == nsGkAtoms::restart) { + parseResult = SetRestart(aValue); + } else { + foundMatch = false; + } + + if (foundMatch) { + aResult.SetTo(aValue); + if (aParseResult) { + *aParseResult = parseResult; + } + } + + return foundMatch; +} + +bool +nsSMILTimedElement::UnsetAttr(nsIAtom* aAttribute) +{ + bool foundMatch = true; + + if (aAttribute == nsGkAtoms::begin) { + UnsetBeginSpec(RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::dur) { + UnsetSimpleDuration(); + } else if (aAttribute == nsGkAtoms::end) { + UnsetEndSpec(RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::fill) { + UnsetFillMode(); + } else if (aAttribute == nsGkAtoms::max) { + UnsetMax(); + } else if (aAttribute == nsGkAtoms::min) { + UnsetMin(); + } else if (aAttribute == nsGkAtoms::repeatCount) { + UnsetRepeatCount(); + } else if (aAttribute == nsGkAtoms::repeatDur) { + UnsetRepeatDur(); + } else if (aAttribute == nsGkAtoms::restart) { + UnsetRestart(); + } else { + foundMatch = false; + } + + return foundMatch; +} + +//---------------------------------------------------------------------- +// Setters and unsetters + +nsresult +nsSMILTimedElement::SetBeginSpec(const nsAString& aBeginSpec, + Element* aContextNode, + RemovalTestFunction aRemove) +{ + return SetBeginOrEndSpec(aBeginSpec, aContextNode, true /*isBegin*/, + aRemove); +} + +void +nsSMILTimedElement::UnsetBeginSpec(RemovalTestFunction aRemove) +{ + ClearSpecs(mBeginSpecs, mBeginInstances, aRemove); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetEndSpec(const nsAString& aEndSpec, + Element* aContextNode, + RemovalTestFunction aRemove) +{ + return SetBeginOrEndSpec(aEndSpec, aContextNode, false /*!isBegin*/, + aRemove); +} + +void +nsSMILTimedElement::UnsetEndSpec(RemovalTestFunction aRemove) +{ + ClearSpecs(mEndSpecs, mEndInstances, aRemove); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetSimpleDuration(const nsAString& aDurSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + const nsAString& dur = nsSMILParserUtils::TrimWhitespace(aDurSpec); + + // SVG-specific: "For SVG's animation elements, if "media" is specified, the + // attribute will be ignored." (SVG 1.1, section 19.2.6) + if (dur.EqualsLiteral("media") || dur.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!nsSMILParserUtils::ParseClockValue(dur, &duration) || + duration.GetMillis() == 0L) { + mSimpleDur.SetIndefinite(); + return NS_ERROR_FAILURE; + } + } + // mSimpleDur should never be unresolved. ParseClockValue will either set + // duration to resolved or will return false. + MOZ_ASSERT(duration.IsResolved(), + "Setting unresolved simple duration"); + + mSimpleDur = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetSimpleDuration() +{ + mSimpleDur.SetIndefinite(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetMin(const nsAString& aMinSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + const nsAString& min = nsSMILParserUtils::TrimWhitespace(aMinSpec); + + if (min.EqualsLiteral("media")) { + duration.SetMillis(0L); + } else { + if (!nsSMILParserUtils::ParseClockValue(min, &duration)) { + mMin.SetMillis(0L); + return NS_ERROR_FAILURE; + } + } + + MOZ_ASSERT(duration.GetMillis() >= 0L, "Invalid duration"); + + mMin = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetMin() +{ + mMin.SetMillis(0L); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetMax(const nsAString& aMaxSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + const nsAString& max = nsSMILParserUtils::TrimWhitespace(aMaxSpec); + + if (max.EqualsLiteral("media") || max.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!nsSMILParserUtils::ParseClockValue(max, &duration) || + duration.GetMillis() == 0L) { + mMax.SetIndefinite(); + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(duration.GetMillis() > 0L, "Invalid duration"); + } + + mMax = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetMax() +{ + mMax.SetIndefinite(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetRestart(const nsAString& aRestartSpec) +{ + nsAttrValue temp; + bool parseResult + = temp.ParseEnumValue(aRestartSpec, sRestartModeTable, true); + mRestartMode = parseResult + ? nsSMILRestartMode(temp.GetEnumValue()) + : RESTART_ALWAYS; + UpdateCurrentInterval(); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILTimedElement::UnsetRestart() +{ + mRestartMode = RESTART_ALWAYS; + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetRepeatCount(const nsAString& aRepeatCountSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILRepeatCount newRepeatCount; + + if (nsSMILParserUtils::ParseRepeatCount(aRepeatCountSpec, newRepeatCount)) { + mRepeatCount = newRepeatCount; + return NS_OK; + } + mRepeatCount.Unset(); + return NS_ERROR_FAILURE; +} + +void +nsSMILTimedElement::UnsetRepeatCount() +{ + mRepeatCount.Unset(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetRepeatDur(const nsAString& aRepeatDurSpec) +{ + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + nsSMILTimeValue duration; + + const nsAString& repeatDur = + nsSMILParserUtils::TrimWhitespace(aRepeatDurSpec); + + if (repeatDur.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!nsSMILParserUtils::ParseClockValue(repeatDur, &duration)) { + mRepeatDur.SetUnresolved(); + return NS_ERROR_FAILURE; + } + } + + mRepeatDur = duration; + + return NS_OK; +} + +void +nsSMILTimedElement::UnsetRepeatDur() +{ + mRepeatDur.SetUnresolved(); + UpdateCurrentInterval(); +} + +nsresult +nsSMILTimedElement::SetFillMode(const nsAString& aFillModeSpec) +{ + uint16_t previousFillMode = mFillMode; + + nsAttrValue temp; + bool parseResult = + temp.ParseEnumValue(aFillModeSpec, sFillModeTable, true); + mFillMode = parseResult + ? nsSMILFillMode(temp.GetEnumValue()) + : FILL_REMOVE; + + // Update fill mode of client + if (mFillMode != previousFillMode && HasClientInFillRange()) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + SampleFillValue(); + } + + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void +nsSMILTimedElement::UnsetFillMode() +{ + uint16_t previousFillMode = mFillMode; + mFillMode = FILL_REMOVE; + if (previousFillMode == FILL_FREEZE && HasClientInFillRange()) { + mClient->Inactivate(false); + } +} + +void +nsSMILTimedElement::AddDependent(nsSMILTimeValueSpec& aDependent) +{ + // There's probably no harm in attempting to register a dependent + // nsSMILTimeValueSpec twice, but we're not expecting it to happen. + MOZ_ASSERT(!mTimeDependents.GetEntry(&aDependent), + "nsSMILTimeValueSpec is already registered as a dependency"); + mTimeDependents.PutEntry(&aDependent); + + // Add current interval. We could add historical intervals too but that would + // cause unpredictable results since some intervals may have been filtered. + // SMIL doesn't say what to do here so for simplicity and consistency we + // simply add the current interval if there is one. + // + // It's not necessary to call SyncPauseTime since we're dealing with + // historical instance times not newly added ones. + if (mCurrentInterval) { + aDependent.HandleNewInterval(*mCurrentInterval, GetTimeContainer()); + } +} + +void +nsSMILTimedElement::RemoveDependent(nsSMILTimeValueSpec& aDependent) +{ + mTimeDependents.RemoveEntry(&aDependent); +} + +bool +nsSMILTimedElement::IsTimeDependent(const nsSMILTimedElement& aOther) const +{ + const nsSMILInstanceTime* thisBegin = GetEffectiveBeginInstance(); + const nsSMILInstanceTime* otherBegin = aOther.GetEffectiveBeginInstance(); + + if (!thisBegin || !otherBegin) + return false; + + return thisBegin->IsDependentOn(*otherBegin); +} + +void +nsSMILTimedElement::BindToTree(nsIContent* aContextNode) +{ + // Reset previously registered milestone since we may be registering with + // a different time container now. + mPrevRegisteredMilestone = sMaxMilestone; + + // If we were already active then clear all our timing information and start + // afresh + if (mElementState != STATE_STARTUP) { + mSeekState = SEEK_NOT_SEEKING; + Rewind(); + } + + // Scope updateBatcher to last only for the ResolveReferences calls: + { + AutoIntervalUpdateBatcher updateBatcher(*this); + + // Resolve references to other parts of the tree + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + mBeginSpecs[i]->ResolveReferences(aContextNode); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + mEndSpecs[j]->ResolveReferences(aContextNode); + } + } + + RegisterMilestone(); +} + +void +nsSMILTimedElement::HandleTargetElementChange(Element* aNewTarget) +{ + AutoIntervalUpdateBatcher updateBatcher(*this); + + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + mBeginSpecs[i]->HandleTargetElementChange(aNewTarget); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + mEndSpecs[j]->HandleTargetElementChange(aNewTarget); + } +} + +void +nsSMILTimedElement::Traverse(nsCycleCollectionTraversalCallback* aCallback) +{ + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsSMILTimeValueSpec* beginSpec = mBeginSpecs[i]; + MOZ_ASSERT(beginSpec, + "null nsSMILTimeValueSpec in list of begin specs"); + beginSpec->Traverse(aCallback); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + nsSMILTimeValueSpec* endSpec = mEndSpecs[j]; + MOZ_ASSERT(endSpec, "null nsSMILTimeValueSpec in list of end specs"); + endSpec->Traverse(aCallback); + } +} + +void +nsSMILTimedElement::Unlink() +{ + AutoIntervalUpdateBatcher updateBatcher(*this); + + // Remove dependencies on other elements + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsSMILTimeValueSpec* beginSpec = mBeginSpecs[i]; + MOZ_ASSERT(beginSpec, + "null nsSMILTimeValueSpec in list of begin specs"); + beginSpec->Unlink(); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + nsSMILTimeValueSpec* endSpec = mEndSpecs[j]; + MOZ_ASSERT(endSpec, "null nsSMILTimeValueSpec in list of end specs"); + endSpec->Unlink(); + } + + ClearIntervals(); + + // Make sure we don't notify other elements of new intervals + mTimeDependents.Clear(); +} + +//---------------------------------------------------------------------- +// Implementation helpers + +nsresult +nsSMILTimedElement::SetBeginOrEndSpec(const nsAString& aSpec, + Element* aContextNode, + bool aIsBegin, + RemovalTestFunction aRemove) +{ + TimeValueSpecList& timeSpecsList = aIsBegin ? mBeginSpecs : mEndSpecs; + InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances; + + ClearSpecs(timeSpecsList, instances, aRemove); + + AutoIntervalUpdateBatcher updateBatcher(*this); + + nsCharSeparatedTokenizer tokenizer(aSpec, ';'); + if (!tokenizer.hasMoreTokens()) { // Empty list + return NS_ERROR_FAILURE; + } + + nsresult rv = NS_OK; + while (tokenizer.hasMoreTokens() && NS_SUCCEEDED(rv)) { + nsAutoPtr<nsSMILTimeValueSpec> + spec(new nsSMILTimeValueSpec(*this, aIsBegin)); + rv = spec->SetSpec(tokenizer.nextToken(), aContextNode); + if (NS_SUCCEEDED(rv)) { + timeSpecsList.AppendElement(spec.forget()); + } + } + + if (NS_FAILED(rv)) { + ClearSpecs(timeSpecsList, instances, aRemove); + } + + return rv; +} + +namespace +{ + // Adaptor functor for RemoveInstanceTimes that allows us to use function + // pointers instead. + // Without this we'd have to either templatize ClearSpecs and all its callers + // or pass bool flags around to specify which removal function to use here. + class MOZ_STACK_CLASS RemoveByFunction + { + public: + explicit RemoveByFunction(nsSMILTimedElement::RemovalTestFunction aFunction) + : mFunction(aFunction) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + return mFunction(aInstanceTime); + } + + private: + nsSMILTimedElement::RemovalTestFunction mFunction; + }; +} // namespace + +void +nsSMILTimedElement::ClearSpecs(TimeValueSpecList& aSpecs, + InstanceTimeList& aInstances, + RemovalTestFunction aRemove) +{ + AutoIntervalUpdateBatcher updateBatcher(*this); + + for (uint32_t i = 0; i < aSpecs.Length(); ++i) { + aSpecs[i]->Unlink(); + } + aSpecs.Clear(); + + RemoveByFunction removeByFunction(aRemove); + RemoveInstanceTimes(aInstances, removeByFunction); +} + +void +nsSMILTimedElement::ClearIntervals() +{ + if (mElementState != STATE_STARTUP) { + mElementState = STATE_POSTACTIVE; + } + mCurrentRepeatIteration = 0; + ResetCurrentInterval(); + + // Remove old intervals + for (int32_t i = mOldIntervals.Length() - 1; i >= 0; --i) { + mOldIntervals[i]->Unlink(); + } + mOldIntervals.Clear(); +} + +bool +nsSMILTimedElement::ApplyEarlyEnd(const nsSMILTimeValue& aSampleTime) +{ + // This should only be called within DoSampleAt as a helper function + MOZ_ASSERT(mElementState == STATE_ACTIVE, + "Unexpected state to try to apply an early end"); + + bool updated = false; + + // Only apply an early end if we're not already ending. + if (mCurrentInterval->End()->Time() > aSampleTime) { + nsSMILInstanceTime* earlyEnd = CheckForEarlyEnd(aSampleTime); + if (earlyEnd) { + if (earlyEnd->IsDependent()) { + // Generate a new instance time for the early end since the + // existing instance time is part of some dependency chain that we + // don't want to participate in. + RefPtr<nsSMILInstanceTime> newEarlyEnd = + new nsSMILInstanceTime(earlyEnd->Time()); + mCurrentInterval->SetEnd(*newEarlyEnd); + } else { + mCurrentInterval->SetEnd(*earlyEnd); + } + updated = true; + } + } + return updated; +} + +namespace +{ + class MOZ_STACK_CLASS RemoveReset + { + public: + explicit RemoveReset(const nsSMILInstanceTime* aCurrentIntervalBegin) + : mCurrentIntervalBegin(aCurrentIntervalBegin) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + // SMIL 3.0 section 5.4.3, 'Resetting element state': + // Any instance times associated with past Event-values, Repeat-values, + // Accesskey-values or added via DOM method calls are removed from the + // dependent begin and end instance times lists. In effect, all events + // and DOM methods calls in the past are cleared. This does not apply to + // an instance time that defines the begin of the current interval. + return aInstanceTime->IsDynamic() && + !aInstanceTime->ShouldPreserve() && + (!mCurrentIntervalBegin || aInstanceTime != mCurrentIntervalBegin); + } + + private: + const nsSMILInstanceTime* mCurrentIntervalBegin; + }; +} // namespace + +void +nsSMILTimedElement::Reset() +{ + RemoveReset resetBegin(mCurrentInterval ? mCurrentInterval->Begin() : nullptr); + RemoveInstanceTimes(mBeginInstances, resetBegin); + + RemoveReset resetEnd(nullptr); + RemoveInstanceTimes(mEndInstances, resetEnd); +} + +void +nsSMILTimedElement::ClearTimingState(RemovalTestFunction aRemove) +{ + mElementState = STATE_STARTUP; + ClearIntervals(); + + UnsetBeginSpec(aRemove); + UnsetEndSpec(aRemove); + + if (mClient) { + mClient->Inactivate(false); + } +} + +void +nsSMILTimedElement::RebuildTimingState(RemovalTestFunction aRemove) +{ + MOZ_ASSERT(mAnimationElement, + "Attempting to enable a timed element not attached to an " + "animation element"); + MOZ_ASSERT(mElementState == STATE_STARTUP, + "Rebuilding timing state from non-startup state"); + + if (mAnimationElement->HasAnimAttr(nsGkAtoms::begin)) { + nsAutoString attValue; + mAnimationElement->GetAnimAttr(nsGkAtoms::begin, attValue); + SetBeginSpec(attValue, mAnimationElement, aRemove); + } + + if (mAnimationElement->HasAnimAttr(nsGkAtoms::end)) { + nsAutoString attValue; + mAnimationElement->GetAnimAttr(nsGkAtoms::end, attValue); + SetEndSpec(attValue, mAnimationElement, aRemove); + } + + mPrevRegisteredMilestone = sMaxMilestone; + RegisterMilestone(); +} + +void +nsSMILTimedElement::DoPostSeek() +{ + // Finish backwards seek + if (mSeekState == SEEK_BACKWARD_FROM_INACTIVE || + mSeekState == SEEK_BACKWARD_FROM_ACTIVE) { + // Previously some dynamic instance times may have been marked to be + // preserved because they were endpoints of an historic interval (which may + // or may not have been filtered). Now that we've finished a seek we should + // clear that flag for those instance times whose intervals are no longer + // historic. + UnpreserveInstanceTimes(mBeginInstances); + UnpreserveInstanceTimes(mEndInstances); + + // Now that the times have been unmarked perform a reset. This might seem + // counter-intuitive when we're only doing a seek within an interval but + // SMIL seems to require this. SMIL 3.0, 'Hyperlinks and timing': + // Resolved end times associated with events, Repeat-values, + // Accesskey-values or added via DOM method calls are cleared when seeking + // to time earlier than the resolved end time. + Reset(); + UpdateCurrentInterval(); + } + + switch (mSeekState) + { + case SEEK_FORWARD_FROM_ACTIVE: + case SEEK_BACKWARD_FROM_ACTIVE: + if (mElementState != STATE_ACTIVE) { + FireTimeEventAsync(eSMILEndEvent, 0); + } + break; + + case SEEK_FORWARD_FROM_INACTIVE: + case SEEK_BACKWARD_FROM_INACTIVE: + if (mElementState == STATE_ACTIVE) { + FireTimeEventAsync(eSMILBeginEvent, 0); + } + break; + + case SEEK_NOT_SEEKING: + /* Do nothing */ + break; + } + + mSeekState = SEEK_NOT_SEEKING; +} + +void +nsSMILTimedElement::UnpreserveInstanceTimes(InstanceTimeList& aList) +{ + const nsSMILInterval* prevInterval = GetPreviousInterval(); + const nsSMILInstanceTime* cutoff = mCurrentInterval ? + mCurrentInterval->Begin() : + prevInterval ? prevInterval->Begin() : nullptr; + uint32_t count = aList.Length(); + for (uint32_t i = 0; i < count; ++i) { + nsSMILInstanceTime* instance = aList[i].get(); + if (!cutoff || cutoff->Time().CompareTo(instance->Time()) < 0) { + instance->UnmarkShouldPreserve(); + } + } +} + +void +nsSMILTimedElement::FilterHistory() +{ + // We should filter the intervals first, since instance times still used in an + // interval won't be filtered. + FilterIntervals(); + FilterInstanceTimes(mBeginInstances); + FilterInstanceTimes(mEndInstances); +} + +void +nsSMILTimedElement::FilterIntervals() +{ + // We can filter old intervals that: + // + // a) are not the previous interval; AND + // b) are not in the middle of a dependency chain; AND + // c) are not the first interval + // + // Condition (a) is necessary since the previous interval is used for applying + // fill effects and updating the current interval. + // + // Condition (b) is necessary since even if this interval itself is not + // active, it may be part of a dependency chain that includes active + // intervals. Such chains are used to establish priorities within the + // animation sandwich. + // + // Condition (c) is necessary to support hyperlinks that target animations + // since in some cases the defined behavior is to seek the document back to + // the first resolved begin time. Presumably the intention here is not + // actually to use the first resolved begin time, the + // _the_first_resolved_begin_time_that_produced_an_interval. That is, + // if we have begin="-5s; -3s; 1s; 3s" with a duration on 1s, we should seek + // to 1s. The spec doesn't say this but I'm pretty sure that is the intention. + // It seems negative times were simply not considered. + // + // Although the above conditions allow us to safely filter intervals for most + // scenarios they do not cover all cases and there will still be scenarios + // that generate intervals indefinitely. In such a case we simply set + // a maximum number of intervals and drop any intervals beyond that threshold. + + uint32_t threshold = mOldIntervals.Length() > sMaxNumIntervals ? + mOldIntervals.Length() - sMaxNumIntervals : + 0; + IntervalList filteredList; + for (uint32_t i = 0; i < mOldIntervals.Length(); ++i) + { + nsSMILInterval* interval = mOldIntervals[i].get(); + if (i != 0 && /*skip first interval*/ + i + 1 < mOldIntervals.Length() && /*skip previous interval*/ + (i < threshold || !interval->IsDependencyChainLink())) { + interval->Unlink(true /*filtered, not deleted*/); + } else { + filteredList.AppendElement(mOldIntervals[i].forget()); + } + } + mOldIntervals.Clear(); + mOldIntervals.SwapElements(filteredList); +} + +namespace +{ + class MOZ_STACK_CLASS RemoveFiltered + { + public: + explicit RemoveFiltered(nsSMILTimeValue aCutoff) : mCutoff(aCutoff) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) + { + // We can filter instance times that: + // a) Precede the end point of the previous interval; AND + // b) Are NOT syncbase times that might be updated to a time after the end + // point of the previous interval; AND + // c) Are NOT fixed end points in any remaining interval. + return aInstanceTime->Time() < mCutoff && + aInstanceTime->IsFixedTime() && + !aInstanceTime->ShouldPreserve(); + } + + private: + nsSMILTimeValue mCutoff; + }; + + class MOZ_STACK_CLASS RemoveBelowThreshold + { + public: + RemoveBelowThreshold(uint32_t aThreshold, + nsTArray<const nsSMILInstanceTime *>& aTimesToKeep) + : mThreshold(aThreshold), + mTimesToKeep(aTimesToKeep) { } + bool operator()(nsSMILInstanceTime* aInstanceTime, uint32_t aIndex) + { + return aIndex < mThreshold && !mTimesToKeep.Contains(aInstanceTime); + } + + private: + uint32_t mThreshold; + nsTArray<const nsSMILInstanceTime *>& mTimesToKeep; + }; +} // namespace + +void +nsSMILTimedElement::FilterInstanceTimes(InstanceTimeList& aList) +{ + if (GetPreviousInterval()) { + RemoveFiltered removeFiltered(GetPreviousInterval()->End()->Time()); + RemoveInstanceTimes(aList, removeFiltered); + } + + // As with intervals it is possible to create a document that, even despite + // our most aggressive filtering, will generate instance times indefinitely + // (e.g. cyclic dependencies with TimeEvents---we can't filter such times as + // they're unpredictable due to the possibility of seeking the document which + // may prevent some events from being generated). Therefore we introduce + // a hard cutoff at which point we just drop the oldest instance times. + if (aList.Length() > sMaxNumInstanceTimes) { + uint32_t threshold = aList.Length() - sMaxNumInstanceTimes; + // There are a few instance times we should keep though, notably: + // - the current interval begin time, + // - the previous interval end time (see note in RemoveInstanceTimes) + // - the first interval begin time (see note in FilterIntervals) + nsTArray<const nsSMILInstanceTime *> timesToKeep; + if (mCurrentInterval) { + timesToKeep.AppendElement(mCurrentInterval->Begin()); + } + const nsSMILInterval* prevInterval = GetPreviousInterval(); + if (prevInterval) { + timesToKeep.AppendElement(prevInterval->End()); + } + if (!mOldIntervals.IsEmpty()) { + timesToKeep.AppendElement(mOldIntervals[0]->Begin()); + } + RemoveBelowThreshold removeBelowThreshold(threshold, timesToKeep); + RemoveInstanceTimes(aList, removeBelowThreshold); + } +} + +// +// This method is based on the pseudocode given in the SMILANIM spec. +// +// See: +// http://www.w3.org/TR/2001/REC-smil-animation-20010904/#Timing-BeginEnd-LC-Start +// +bool +nsSMILTimedElement::GetNextInterval(const nsSMILInterval* aPrevInterval, + const nsSMILInterval* aReplacedInterval, + const nsSMILInstanceTime* aFixedBeginTime, + nsSMILInterval& aResult) const +{ + MOZ_ASSERT(!aFixedBeginTime || aFixedBeginTime->Time().IsDefinite(), + "Unresolved or indefinite begin time given for interval start"); + static const nsSMILTimeValue zeroTime(0L); + + if (mRestartMode == RESTART_NEVER && aPrevInterval) + return false; + + // Calc starting point + nsSMILTimeValue beginAfter; + bool prevIntervalWasZeroDur = false; + if (aPrevInterval) { + beginAfter = aPrevInterval->End()->Time(); + prevIntervalWasZeroDur + = aPrevInterval->End()->Time() == aPrevInterval->Begin()->Time(); + } else { + beginAfter.SetMillis(INT64_MIN); + } + + RefPtr<nsSMILInstanceTime> tempBegin; + RefPtr<nsSMILInstanceTime> tempEnd; + + while (true) { + // Calculate begin time + if (aFixedBeginTime) { + if (aFixedBeginTime->Time() < beginAfter) { + return false; + } + // our ref-counting is not const-correct + tempBegin = const_cast<nsSMILInstanceTime*>(aFixedBeginTime); + } else if ((!mAnimationElement || + !mAnimationElement->HasAnimAttr(nsGkAtoms::begin)) && + beginAfter <= zeroTime) { + tempBegin = new nsSMILInstanceTime(nsSMILTimeValue(0)); + } else { + int32_t beginPos = 0; + do { + tempBegin = + GetNextGreaterOrEqual(mBeginInstances, beginAfter, beginPos); + if (!tempBegin || !tempBegin->Time().IsDefinite()) { + return false; + } + // If we're updating the current interval then skip any begin time that is + // dependent on the current interval's begin time. e.g. + // <animate id="a" begin="b.begin; a.begin+2s"... + // If b's interval disappears whilst 'a' is in the waiting state the begin + // time at "a.begin+2s" should be skipped since 'a' never begun. + } while (aReplacedInterval && + tempBegin->GetBaseTime() == aReplacedInterval->Begin()); + } + MOZ_ASSERT(tempBegin && tempBegin->Time().IsDefinite() && + tempBegin->Time() >= beginAfter, + "Got a bad begin time while fetching next interval"); + + // Calculate end time + { + int32_t endPos = 0; + do { + tempEnd = + GetNextGreaterOrEqual(mEndInstances, tempBegin->Time(), endPos); + + // SMIL doesn't allow for coincident zero-duration intervals, so if the + // previous interval was zero-duration, and tempEnd is going to give us + // another zero duration interval, then look for another end to use + // instead. + if (tempEnd && prevIntervalWasZeroDur && + tempEnd->Time() == beginAfter) { + tempEnd = GetNextGreater(mEndInstances, tempBegin->Time(), endPos); + } + // As above with begin times, avoid creating self-referential loops + // between instance times by checking that the newly found end instance + // time is not already dependent on the end of the current interval. + } while (tempEnd && aReplacedInterval && + tempEnd->GetBaseTime() == aReplacedInterval->End()); + + if (!tempEnd) { + // If all the ends are before the beginning we have a bad interval + // UNLESS: + // a) We never had any end attribute to begin with (the SMIL pseudocode + // places this condition earlier in the flow but that fails to allow + // for DOM calls when no "indefinite" condition is given), OR + // b) We never had any end instance times to begin with, OR + // c) We have end events which leave the interval open-ended. + bool openEndedIntervalOk = mEndSpecs.IsEmpty() || + mEndInstances.IsEmpty() || + EndHasEventConditions(); + + // The above conditions correspond with the SMIL pseudocode but SMIL + // doesn't address self-dependent instance times which we choose to + // ignore. + // + // Therefore we add a qualification of (b) above that even if + // there are end instance times but they all depend on the end of the + // current interval we should act as if they didn't exist and allow the + // open-ended interval. + // + // In the following condition we don't use |= because it doesn't provide + // short-circuit behavior. + openEndedIntervalOk = openEndedIntervalOk || + (aReplacedInterval && + AreEndTimesDependentOn(aReplacedInterval->End())); + + if (!openEndedIntervalOk) { + return false; // Bad interval + } + } + + nsSMILTimeValue intervalEnd = tempEnd + ? tempEnd->Time() : nsSMILTimeValue(); + nsSMILTimeValue activeEnd = CalcActiveEnd(tempBegin->Time(), intervalEnd); + + if (!tempEnd || intervalEnd != activeEnd) { + tempEnd = new nsSMILInstanceTime(activeEnd); + } + } + MOZ_ASSERT(tempEnd, "Failed to get end point for next interval"); + + // When we choose the interval endpoints, we don't allow coincident + // zero-duration intervals, so if we arrive here and we have a zero-duration + // interval starting at the same point as a previous zero-duration interval, + // then it must be because we've applied constraints to the active duration. + // In that case, we will potentially run into an infinite loop, so we break + // it by searching for the next interval that starts AFTER our current + // zero-duration interval. + if (prevIntervalWasZeroDur && tempEnd->Time() == beginAfter) { + if (prevIntervalWasZeroDur) { + beginAfter.SetMillis(tempBegin->Time().GetMillis() + 1); + prevIntervalWasZeroDur = false; + continue; + } + } + prevIntervalWasZeroDur = tempBegin->Time() == tempEnd->Time(); + + // Check for valid interval + if (tempEnd->Time() > zeroTime || + (tempBegin->Time() == zeroTime && tempEnd->Time() == zeroTime)) { + aResult.Set(*tempBegin, *tempEnd); + return true; + } + + if (mRestartMode == RESTART_NEVER) { + // tempEnd <= 0 so we're going to loop which effectively means restarting + return false; + } + + beginAfter = tempEnd->Time(); + } + NS_NOTREACHED("Hmm... we really shouldn't be here"); + + return false; +} + +nsSMILInstanceTime* +nsSMILTimedElement::GetNextGreater(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const +{ + nsSMILInstanceTime* result = nullptr; + while ((result = GetNextGreaterOrEqual(aList, aBase, aPosition)) && + result->Time() == aBase) { } + return result; +} + +nsSMILInstanceTime* +nsSMILTimedElement::GetNextGreaterOrEqual(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const +{ + nsSMILInstanceTime* result = nullptr; + int32_t count = aList.Length(); + + for (; aPosition < count && !result; ++aPosition) { + nsSMILInstanceTime* val = aList[aPosition].get(); + MOZ_ASSERT(val, "NULL instance time in list"); + if (val->Time() >= aBase) { + result = val; + } + } + + return result; +} + +/** + * @see SMILANIM 3.3.4 + */ +nsSMILTimeValue +nsSMILTimedElement::CalcActiveEnd(const nsSMILTimeValue& aBegin, + const nsSMILTimeValue& aEnd) const +{ + nsSMILTimeValue result; + + MOZ_ASSERT(mSimpleDur.IsResolved(), + "Unresolved simple duration in CalcActiveEnd"); + MOZ_ASSERT(aBegin.IsDefinite(), + "Indefinite or unresolved begin time in CalcActiveEnd"); + + result = GetRepeatDuration(); + + if (aEnd.IsDefinite()) { + nsSMILTime activeDur = aEnd.GetMillis() - aBegin.GetMillis(); + + if (result.IsDefinite()) { + result.SetMillis(std::min(result.GetMillis(), activeDur)); + } else { + result.SetMillis(activeDur); + } + } + + result = ApplyMinAndMax(result); + + if (result.IsDefinite()) { + nsSMILTime activeEnd = result.GetMillis() + aBegin.GetMillis(); + result.SetMillis(activeEnd); + } + + return result; +} + +nsSMILTimeValue +nsSMILTimedElement::GetRepeatDuration() const +{ + nsSMILTimeValue multipliedDuration; + if (mRepeatCount.IsDefinite() && mSimpleDur.IsDefinite()) { + multipliedDuration.SetMillis( + nsSMILTime(mRepeatCount * double(mSimpleDur.GetMillis()))); + } else { + multipliedDuration.SetIndefinite(); + } + + nsSMILTimeValue repeatDuration; + + if (mRepeatDur.IsResolved()) { + repeatDuration = std::min(multipliedDuration, mRepeatDur); + } else if (mRepeatCount.IsSet()) { + repeatDuration = multipliedDuration; + } else { + repeatDuration = mSimpleDur; + } + + return repeatDuration; +} + +nsSMILTimeValue +nsSMILTimedElement::ApplyMinAndMax(const nsSMILTimeValue& aDuration) const +{ + if (!aDuration.IsResolved()) { + return aDuration; + } + + if (mMax < mMin) { + return aDuration; + } + + nsSMILTimeValue result; + + if (aDuration > mMax) { + result = mMax; + } else if (aDuration < mMin) { + result = mMin; + } else { + result = aDuration; + } + + return result; +} + +nsSMILTime +nsSMILTimedElement::ActiveTimeToSimpleTime(nsSMILTime aActiveTime, + uint32_t& aRepeatIteration) +{ + nsSMILTime result; + + MOZ_ASSERT(mSimpleDur.IsResolved(), + "Unresolved simple duration in ActiveTimeToSimpleTime"); + MOZ_ASSERT(aActiveTime >= 0, "Expecting non-negative active time"); + // Note that a negative aActiveTime will give us a negative value for + // aRepeatIteration, which is bad because aRepeatIteration is unsigned + + if (mSimpleDur.IsIndefinite() || mSimpleDur.GetMillis() == 0L) { + aRepeatIteration = 0; + result = aActiveTime; + } else { + result = aActiveTime % mSimpleDur.GetMillis(); + aRepeatIteration = (uint32_t)(aActiveTime / mSimpleDur.GetMillis()); + } + + return result; +} + +// +// Although in many cases it would be possible to check for an early end and +// adjust the current interval well in advance the SMIL Animation spec seems to +// indicate that we should only apply an early end at the latest possible +// moment. In particular, this paragraph from section 3.6.8: +// +// 'If restart is set to "always", then the current interval will end early if +// there is an instance time in the begin list that is before (i.e. earlier +// than) the defined end for the current interval. Ending in this manner will +// also send a changed time notice to all time dependents for the current +// interval end.' +// +nsSMILInstanceTime* +nsSMILTimedElement::CheckForEarlyEnd( + const nsSMILTimeValue& aContainerTime) const +{ + MOZ_ASSERT(mCurrentInterval, + "Checking for an early end but the current interval is not set"); + if (mRestartMode != RESTART_ALWAYS) + return nullptr; + + int32_t position = 0; + nsSMILInstanceTime* nextBegin = + GetNextGreater(mBeginInstances, mCurrentInterval->Begin()->Time(), + position); + + if (nextBegin && + nextBegin->Time() > mCurrentInterval->Begin()->Time() && + nextBegin->Time() < mCurrentInterval->End()->Time() && + nextBegin->Time() <= aContainerTime) { + return nextBegin; + } + + return nullptr; +} + +void +nsSMILTimedElement::UpdateCurrentInterval(bool aForceChangeNotice) +{ + // Check if updates are currently blocked (batched) + if (mDeferIntervalUpdates) { + mDoDeferredUpdate = true; + return; + } + + // We adopt the convention of not resolving intervals until the first + // sample. Otherwise, every time each attribute is set we'll re-resolve the + // current interval and notify all our time dependents of the change. + // + // The disadvantage of deferring resolving the interval is that DOM calls to + // to getStartTime will throw an INVALID_STATE_ERR exception until the + // document timeline begins since the start time has not yet been resolved. + if (mElementState == STATE_STARTUP) + return; + + // Although SMIL gives rules for detecting cycles in change notifications, + // some configurations can lead to create-delete-create-delete-etc. cycles + // which SMIL does not consider. + // + // In order to provide consistent behavior in such cases, we detect two + // deletes in a row and then refuse to create any further intervals. That is, + // we say the configuration is invalid. + if (mDeleteCount > 1) { + // When we update the delete count we also set the state to post active, so + // if we're not post active here then something other than + // UpdateCurrentInterval has updated the element state in between and all + // bets are off. + MOZ_ASSERT(mElementState == STATE_POSTACTIVE, + "Expected to be in post-active state after performing double " + "delete"); + return; + } + + // Check that we aren't stuck in infinite recursion updating some syncbase + // dependencies. Generally such situations should be detected in advance and + // the chain broken in a sensible and predictable manner, so if we're hitting + // this assertion we need to work out how to detect the case that's causing + // it. In release builds, just bail out before we overflow the stack. + AutoRestore<uint8_t> depthRestorer(mUpdateIntervalRecursionDepth); + if (++mUpdateIntervalRecursionDepth > sMaxUpdateIntervalRecursionDepth) { + MOZ_ASSERT(false, + "Update current interval recursion depth exceeded threshold"); + return; + } + + // If the interval is active the begin time is fixed. + const nsSMILInstanceTime* beginTime = mElementState == STATE_ACTIVE + ? mCurrentInterval->Begin() + : nullptr; + nsSMILInterval updatedInterval; + if (GetNextInterval(GetPreviousInterval(), mCurrentInterval, + beginTime, updatedInterval)) { + + if (mElementState == STATE_POSTACTIVE) { + + MOZ_ASSERT(!mCurrentInterval, + "In postactive state but the interval has been set"); + mCurrentInterval = new nsSMILInterval(updatedInterval); + mElementState = STATE_WAITING; + NotifyNewInterval(); + + } else { + + bool beginChanged = false; + bool endChanged = false; + + if (mElementState != STATE_ACTIVE && + !updatedInterval.Begin()->SameTimeAndBase( + *mCurrentInterval->Begin())) { + mCurrentInterval->SetBegin(*updatedInterval.Begin()); + beginChanged = true; + } + + if (!updatedInterval.End()->SameTimeAndBase(*mCurrentInterval->End())) { + mCurrentInterval->SetEnd(*updatedInterval.End()); + endChanged = true; + } + + if (beginChanged || endChanged || aForceChangeNotice) { + NotifyChangedInterval(mCurrentInterval, beginChanged, endChanged); + } + } + + // There's a chance our next milestone has now changed, so update the time + // container + RegisterMilestone(); + } else { // GetNextInterval failed: Current interval is no longer valid + if (mElementState == STATE_ACTIVE) { + // The interval is active so we can't just delete it, instead trim it so + // that begin==end. + if (!mCurrentInterval->End()->SameTimeAndBase(*mCurrentInterval->Begin())) + { + mCurrentInterval->SetEnd(*mCurrentInterval->Begin()); + NotifyChangedInterval(mCurrentInterval, false, true); + } + // The transition to the postactive state will take place on the next + // sample (along with firing end events, clearing intervals etc.) + RegisterMilestone(); + } else if (mElementState == STATE_WAITING) { + AutoRestore<uint8_t> deleteCountRestorer(mDeleteCount); + ++mDeleteCount; + mElementState = STATE_POSTACTIVE; + ResetCurrentInterval(); + } + } +} + +void +nsSMILTimedElement::SampleSimpleTime(nsSMILTime aActiveTime) +{ + if (mClient) { + uint32_t repeatIteration; + nsSMILTime simpleTime = + ActiveTimeToSimpleTime(aActiveTime, repeatIteration); + mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration); + } +} + +void +nsSMILTimedElement::SampleFillValue() +{ + if (mFillMode != FILL_FREEZE || !mClient) + return; + + nsSMILTime activeTime; + + if (mElementState == STATE_WAITING || mElementState == STATE_POSTACTIVE) { + const nsSMILInterval* prevInterval = GetPreviousInterval(); + MOZ_ASSERT(prevInterval, + "Attempting to sample fill value but there is no previous " + "interval"); + MOZ_ASSERT(prevInterval->End()->Time().IsDefinite() && + prevInterval->End()->IsFixedTime(), + "Attempting to sample fill value but the endpoint of the " + "previous interval is not resolved and fixed"); + + activeTime = prevInterval->End()->Time().GetMillis() - + prevInterval->Begin()->Time().GetMillis(); + + // If the interval's repeat duration was shorter than its active duration, + // use the end of the repeat duration to determine the frozen animation's + // state. + nsSMILTimeValue repeatDuration = GetRepeatDuration(); + if (repeatDuration.IsDefinite()) { + activeTime = std::min(repeatDuration.GetMillis(), activeTime); + } + } else { + MOZ_ASSERT(mElementState == STATE_ACTIVE, + "Attempting to sample fill value when we're in an unexpected state " + "(probably STATE_STARTUP)"); + + // If we are being asked to sample the fill value while active we *must* + // have a repeat duration shorter than the active duration so use that. + MOZ_ASSERT(GetRepeatDuration().IsDefinite(), + "Attempting to sample fill value of an active animation with " + "an indefinite repeat duration"); + activeTime = GetRepeatDuration().GetMillis(); + } + + uint32_t repeatIteration; + nsSMILTime simpleTime = + ActiveTimeToSimpleTime(activeTime, repeatIteration); + + if (simpleTime == 0L && repeatIteration) { + mClient->SampleLastValue(--repeatIteration); + } else { + mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration); + } +} + +nsresult +nsSMILTimedElement::AddInstanceTimeFromCurrentTime(nsSMILTime aCurrentTime, + double aOffsetSeconds, bool aIsBegin) +{ + double offset = aOffsetSeconds * PR_MSEC_PER_SEC; + + // Check we won't overflow the range of nsSMILTime + if (aCurrentTime + NS_round(offset) > INT64_MAX) + return NS_ERROR_ILLEGAL_VALUE; + + nsSMILTimeValue timeVal(aCurrentTime + int64_t(NS_round(offset))); + + RefPtr<nsSMILInstanceTime> instanceTime = + new nsSMILInstanceTime(timeVal, nsSMILInstanceTime::SOURCE_DOM); + + AddInstanceTime(instanceTime, aIsBegin); + + return NS_OK; +} + +void +nsSMILTimedElement::RegisterMilestone() +{ + nsSMILTimeContainer* container = GetTimeContainer(); + if (!container) + return; + MOZ_ASSERT(mAnimationElement, + "Got a time container without an owning animation element"); + + nsSMILMilestone nextMilestone; + if (!GetNextMilestone(nextMilestone)) + return; + + // This method is called every time we might possibly have updated our + // current interval, but since nsSMILTimeContainer makes no attempt to filter + // out redundant milestones we do some rudimentary filtering here. It's not + // perfect, but unnecessary samples are fairly cheap. + if (nextMilestone >= mPrevRegisteredMilestone) + return; + + container->AddMilestone(nextMilestone, *mAnimationElement); + mPrevRegisteredMilestone = nextMilestone; +} + +bool +nsSMILTimedElement::GetNextMilestone(nsSMILMilestone& aNextMilestone) const +{ + // Return the next key moment in our lifetime. + // + // XXX It may be possible in future to optimise this so that we only register + // for milestones if: + // a) We have time dependents, or + // b) We are dependent on events or syncbase relationships, or + // c) There are registered listeners for our events + // + // Then for the simple case where everything uses offset values we could + // ignore milestones altogether. + // + // We'd need to be careful, however, that if one of those conditions became + // true in between samples that we registered our next milestone at that + // point. + + switch (mElementState) + { + case STATE_STARTUP: + // All elements register for an initial end sample at t=0 where we resolve + // our initial interval. + aNextMilestone.mIsEnd = true; // Initial sample should be an end sample + aNextMilestone.mTime = 0; + return true; + + case STATE_WAITING: + MOZ_ASSERT(mCurrentInterval, + "In waiting state but the current interval has not been set"); + aNextMilestone.mIsEnd = false; + aNextMilestone.mTime = mCurrentInterval->Begin()->Time().GetMillis(); + return true; + + case STATE_ACTIVE: + { + // Work out what comes next: the interval end or the next repeat iteration + nsSMILTimeValue nextRepeat; + if (mSeekState == SEEK_NOT_SEEKING && mSimpleDur.IsDefinite()) { + nsSMILTime nextRepeatActiveTime = + (mCurrentRepeatIteration + 1) * mSimpleDur.GetMillis(); + // Check that the repeat fits within the repeat duration + if (nsSMILTimeValue(nextRepeatActiveTime) < GetRepeatDuration()) { + nextRepeat.SetMillis(mCurrentInterval->Begin()->Time().GetMillis() + + nextRepeatActiveTime); + } + } + nsSMILTimeValue nextMilestone = + std::min(mCurrentInterval->End()->Time(), nextRepeat); + + // Check for an early end before that time + nsSMILInstanceTime* earlyEnd = CheckForEarlyEnd(nextMilestone); + if (earlyEnd) { + aNextMilestone.mIsEnd = true; + aNextMilestone.mTime = earlyEnd->Time().GetMillis(); + return true; + } + + // Apply the previously calculated milestone + if (nextMilestone.IsDefinite()) { + aNextMilestone.mIsEnd = nextMilestone != nextRepeat; + aNextMilestone.mTime = nextMilestone.GetMillis(); + return true; + } + + return false; + } + + case STATE_POSTACTIVE: + return false; + } + MOZ_CRASH("Invalid element state"); +} + +void +nsSMILTimedElement::NotifyNewInterval() +{ + MOZ_ASSERT(mCurrentInterval, + "Attempting to notify dependents of a new interval but the " + "interval is not set"); + + nsSMILTimeContainer* container = GetTimeContainer(); + if (container) { + container->SyncPauseTime(); + } + + for (auto iter = mTimeDependents.Iter(); !iter.Done(); iter.Next()) { + nsSMILInterval* interval = mCurrentInterval; + // It's possible that in notifying one new time dependent of a new interval + // that a chain reaction is triggered which results in the original + // interval disappearing. If that's the case we can skip sending further + // notifications. + if (!interval) { + break; + } + nsSMILTimeValueSpec* spec = iter.Get()->GetKey(); + spec->HandleNewInterval(*interval, container); + } +} + +void +nsSMILTimedElement::NotifyChangedInterval(nsSMILInterval* aInterval, + bool aBeginObjectChanged, + bool aEndObjectChanged) +{ + MOZ_ASSERT(aInterval, "Null interval for change notification"); + + nsSMILTimeContainer* container = GetTimeContainer(); + if (container) { + container->SyncPauseTime(); + } + + // Copy the instance times list since notifying the instance times can result + // in a chain reaction whereby our own interval gets deleted along with its + // instance times. + InstanceTimeList times; + aInterval->GetDependentTimes(times); + + for (uint32_t i = 0; i < times.Length(); ++i) { + times[i]->HandleChangedInterval(container, aBeginObjectChanged, + aEndObjectChanged); + } +} + +void +nsSMILTimedElement::FireTimeEventAsync(EventMessage aMsg, int32_t aDetail) +{ + if (!mAnimationElement) + return; + + nsCOMPtr<nsIRunnable> event = + new AsyncTimeEventRunner(mAnimationElement, aMsg, aDetail); + NS_DispatchToMainThread(event); +} + +const nsSMILInstanceTime* +nsSMILTimedElement::GetEffectiveBeginInstance() const +{ + switch (mElementState) + { + case STATE_STARTUP: + return nullptr; + + case STATE_ACTIVE: + return mCurrentInterval->Begin(); + + case STATE_WAITING: + case STATE_POSTACTIVE: + { + const nsSMILInterval* prevInterval = GetPreviousInterval(); + return prevInterval ? prevInterval->Begin() : nullptr; + } + } + MOZ_CRASH("Invalid element state"); +} + +const nsSMILInterval* +nsSMILTimedElement::GetPreviousInterval() const +{ + return mOldIntervals.IsEmpty() + ? nullptr + : mOldIntervals[mOldIntervals.Length()-1].get(); +} + +bool +nsSMILTimedElement::HasClientInFillRange() const +{ + // Returns true if we have a client that is in the range where it will fill + return mClient && + ((mElementState != STATE_ACTIVE && HasPlayed()) || + (mElementState == STATE_ACTIVE && !mClient->IsActive())); +} + +bool +nsSMILTimedElement::EndHasEventConditions() const +{ + for (uint32_t i = 0; i < mEndSpecs.Length(); ++i) { + if (mEndSpecs[i]->IsEventBased()) + return true; + } + return false; +} + +bool +nsSMILTimedElement::AreEndTimesDependentOn( + const nsSMILInstanceTime* aBase) const +{ + if (mEndInstances.IsEmpty()) + return false; + + for (uint32_t i = 0; i < mEndInstances.Length(); ++i) { + if (mEndInstances[i]->GetBaseTime() != aBase) { + return false; + } + } + return true; +} + diff --git a/dom/smil/nsSMILTimedElement.h b/dom/smil/nsSMILTimedElement.h new file mode 100644 index 000000000..1831deeb0 --- /dev/null +++ b/dom/smil/nsSMILTimedElement.h @@ -0,0 +1,673 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILTIMEDELEMENT_H_ +#define NS_SMILTIMEDELEMENT_H_ + +#include "mozilla/EventForwards.h" +#include "mozilla/Move.h" +#include "nsSMILInterval.h" +#include "nsSMILInstanceTime.h" +#include "nsSMILMilestone.h" +#include "nsSMILTimeValueSpec.h" +#include "nsSMILRepeatCount.h" +#include "nsSMILTypes.h" +#include "nsTArray.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsAutoPtr.h" +#include "nsAttrValue.h" + +class nsSMILAnimationFunction; +class nsSMILTimeContainer; +class nsSMILTimeValue; +class nsIAtom; + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom +} // namespace mozilla + +//---------------------------------------------------------------------- +// nsSMILTimedElement + +class nsSMILTimedElement +{ +public: + nsSMILTimedElement(); + ~nsSMILTimedElement(); + + typedef mozilla::dom::Element Element; + + /* + * Sets the owning animation element which this class uses to convert between + * container times and to register timebase elements. + */ + void SetAnimationElement(mozilla::dom::SVGAnimationElement* aElement); + + /* + * Returns the time container with which this timed element is associated or + * nullptr if it is not associated with a time container. + */ + nsSMILTimeContainer* GetTimeContainer(); + + /* + * Returns the element targeted by the animation element. Needed for + * registering event listeners against the appropriate element. + */ + mozilla::dom::Element* GetTargetElement(); + + /** + * Methods for supporting the nsIDOMElementTimeControl interface. + */ + + /* + * Adds a new begin instance time at the current container time plus or minus + * the specified offset. + * + * @param aOffsetSeconds A real number specifying the number of seconds to add + * to the current container time. + * @return NS_OK if the operation succeeeded, or an error code otherwise. + */ + nsresult BeginElementAt(double aOffsetSeconds); + + /* + * Adds a new end instance time at the current container time plus or minus + * the specified offset. + * + * @param aOffsetSeconds A real number specifying the number of seconds to add + * to the current container time. + * @return NS_OK if the operation succeeeded, or an error code otherwise. + */ + nsresult EndElementAt(double aOffsetSeconds); + + /** + * Methods for supporting the nsSVGAnimationElement interface. + */ + + /** + * According to SVG 1.1 SE this returns + * + * the begin time, in seconds, for this animation element's current + * interval, if it exists, regardless of whether the interval has begun yet. + * + * @return the start time as defined above in milliseconds or an unresolved + * time if there is no current interval. + */ + nsSMILTimeValue GetStartTime() const; + + /** + * Returns the simple duration of this element. + * + * @return the simple duration in milliseconds or INDEFINITE. + */ + nsSMILTimeValue GetSimpleDuration() const + { + return mSimpleDur; + } + + /** + * Methods for supporting hyperlinking + */ + + /** + * Internal SMIL methods + */ + + /** + * Returns the time to seek the document to when this element is targetted by + * a hyperlink. + * + * The behavior is defined here: + * http://www.w3.org/TR/smil-animation/#HyperlinkSemantics + * + * It is very similar to GetStartTime() with the exception that when the + * element is not active, the begin time of the *first* interval is returned. + * + * @return the time to seek the documen to in milliseconds or an unresolved + * time if there is no resolved interval. + */ + nsSMILTimeValue GetHyperlinkTime() const; + + /** + * Adds an instance time object this element's list of instance times. + * These instance times are used when creating intervals. + * + * This method is typically called by an nsSMILTimeValueSpec. + * + * @param aInstanceTime The time to add, expressed in container time. + * @param aIsBegin true if the time to be added represents a begin + * time or false if it represents an end time. + */ + void AddInstanceTime(nsSMILInstanceTime* aInstanceTime, bool aIsBegin); + + /** + * Requests this element update the given instance time. + * + * This method is typically called by a child nsSMILTimeValueSpec. + * + * @param aInstanceTime The instance time to update. + * @param aUpdatedTime The time to update aInstanceTime with. + * @param aDependentTime The instance time upon which aInstanceTime should be + * based. + * @param aIsBegin true if the time to be updated represents a begin + * instance time or false if it represents an end + * instance time. + */ + void UpdateInstanceTime(nsSMILInstanceTime* aInstanceTime, + nsSMILTimeValue& aUpdatedTime, + bool aIsBegin); + + /** + * Removes an instance time object from this element's list of instance times. + * + * This method is typically called by a child nsSMILTimeValueSpec. + * + * @param aInstanceTime The instance time to remove. + * @param aIsBegin true if the time to be removed represents a begin + * time or false if it represents an end time. + */ + void RemoveInstanceTime(nsSMILInstanceTime* aInstanceTime, bool aIsBegin); + + /** + * Removes all the instance times associated with the given + * nsSMILTimeValueSpec object. Used when an ID assignment changes and hence + * all the previously associated instance times become invalid. + * + * @param aSpec The nsSMILTimeValueSpec object whose created + * nsSMILInstanceTime's should be removed. + * @param aIsBegin true if the times to be removed represent begin + * times or false if they are end times. + */ + void RemoveInstanceTimesForCreator(const nsSMILTimeValueSpec* aSpec, + bool aIsBegin); + + /** + * Sets the object that will be called by this timed element each time it is + * sampled. + * + * In Schmitz's model it is possible to associate several time clients with + * a timed element but for now we only allow one. + * + * @param aClient The time client to associate. Any previous time client + * will be disassociated and no longer sampled. Setting this + * to nullptr will simply disassociate the previous client, if + * any. + */ + void SetTimeClient(nsSMILAnimationFunction* aClient); + + /** + * Samples the object at the given container time. Timing intervals are + * updated and if this element is active at the given time the associated time + * client will be sampled with the appropriate simple time. + * + * @param aContainerTime The container time at which to sample. + */ + void SampleAt(nsSMILTime aContainerTime); + + /** + * Performs a special sample for the end of an interval. Such a sample should + * only advance the timed element (and any dependent elements) to the waiting + * or postactive state. It should not cause a transition to the active state. + * Transition to the active state is only performed on a regular SampleAt. + * + * This allows all interval ends at a given time to be processed first and + * hence the new interval can be established based on full information of the + * available instance times. + * + * @param aContainerTime The container time at which to sample. + */ + void SampleEndAt(nsSMILTime aContainerTime); + + /** + * Informs the timed element that its time container has changed time + * relative to document time. The timed element therefore needs to update its + * dependent elements (which may belong to a different time container) so they + * can re-resolve their times. + */ + void HandleContainerTimeChange(); + + /** + * Resets this timed element's accumulated times and intervals back to start + * up state. + * + * This is used for backwards seeking where rather than accumulating + * historical timing state and winding it back, we reset the element and seek + * forwards. + */ + void Rewind(); + + /** + * Marks this element as disabled or not. If the element is disabled, it + * will ignore any future samples and discard any accumulated timing state. + * + * This is used by SVG to "turn off" timed elements when the associated + * animation element has failing conditional processing tests. + * + * Returns true if the disabled state of the timed element was changed + * as a result of this call (i.e. it was not a redundant call). + */ + bool SetIsDisabled(bool aIsDisabled); + + /** + * Attempts to set an attribute on this timed element. + * + * @param aAttribute The name of the attribute to set. The namespace of this + * attribute is not specified as it is checked by the host + * element. Only attributes in the namespace defined for + * SMIL attributes in the host language are passed to the + * timed element. + * @param aValue The attribute value. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aContextNode The element to use for context when resolving + * references to other elements. + * @param[out] aParseResult The result of parsing the attribute. Will be set + * to NS_OK if parsing is successful. + * + * @return true if the given attribute is a timing attribute, false + * otherwise. + */ + bool SetAttr(nsIAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, Element* aContextNode, + nsresult* aParseResult = nullptr); + + /** + * Attempts to unset an attribute on this timed element. + * + * @param aAttribute The name of the attribute to set. As with SetAttr the + * namespace of the attribute is not specified (see + * SetAttr). + * + * @return true if the given attribute is a timing attribute, false + * otherwise. + */ + bool UnsetAttr(nsIAtom* aAttribute); + + /** + * Adds a syncbase dependency to the list of dependents that will be notified + * when this timed element creates, deletes, or updates its current interval. + * + * @param aDependent The nsSMILTimeValueSpec object to notify. A raw pointer + * to this object will be stored. Therefore it is necessary + * for the object to be explicitly unregistered (with + * RemoveDependent) when it is destroyed. + */ + void AddDependent(nsSMILTimeValueSpec& aDependent); + + /** + * Removes a syncbase dependency from the list of dependents that are notified + * when the current interval is modified. + * + * @param aDependent The nsSMILTimeValueSpec object to unregister. + */ + void RemoveDependent(nsSMILTimeValueSpec& aDependent); + + /** + * Determines if this timed element is dependent on the given timed element's + * begin time for the interval currently in effect. Whilst the element is in + * the active state this is the current interval and in the postactive or + * waiting state this is the previous interval if one exists. In all other + * cases the element is not considered a time dependent of any other element. + * + * @param aOther The potential syncbase element. + * @return true if this timed element's begin time for the currently + * effective interval is directly or indirectly derived from aOther, false + * otherwise. + */ + bool IsTimeDependent(const nsSMILTimedElement& aOther) const; + + /** + * Called when the timed element has been bound to the document so that + * references from this timed element to other elements can be resolved. + * + * @param aContextNode The node which provides the necessary context for + * resolving references. This is typically the element in + * the host language that owns this timed element. Should + * not be null. + */ + void BindToTree(nsIContent* aContextNode); + + /** + * Called when the target of the animation has changed so that event + * registrations can be updated. + */ + void HandleTargetElementChange(mozilla::dom::Element* aNewTarget); + + /** + * Called when the timed element has been removed from a document so that + * references to other elements can be broken. + */ + void DissolveReferences() { Unlink(); } + + // Cycle collection + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + typedef bool (*RemovalTestFunction)(nsSMILInstanceTime* aInstance); + +protected: + // Typedefs + typedef nsTArray<nsAutoPtr<nsSMILTimeValueSpec> > TimeValueSpecList; + typedef nsTArray<RefPtr<nsSMILInstanceTime> > InstanceTimeList; + typedef nsTArray<nsAutoPtr<nsSMILInterval> > IntervalList; + typedef nsPtrHashKey<nsSMILTimeValueSpec> TimeValueSpecPtrKey; + typedef nsTHashtable<TimeValueSpecPtrKey> TimeValueSpecHashSet; + + // Helper classes + class InstanceTimeComparator { + public: + bool Equals(const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const; + bool LessThan(const nsSMILInstanceTime* aElem1, + const nsSMILInstanceTime* aElem2) const; + }; + + // Templated helper functions + template <class TestFunctor> + void RemoveInstanceTimes(InstanceTimeList& aArray, TestFunctor& aTest); + + // + // Implementation helpers + // + + nsresult SetBeginSpec(const nsAString& aBeginSpec, + Element* aContextNode, + RemovalTestFunction aRemove); + nsresult SetEndSpec(const nsAString& aEndSpec, + Element* aContextNode, + RemovalTestFunction aRemove); + nsresult SetSimpleDuration(const nsAString& aDurSpec); + nsresult SetMin(const nsAString& aMinSpec); + nsresult SetMax(const nsAString& aMaxSpec); + nsresult SetRestart(const nsAString& aRestartSpec); + nsresult SetRepeatCount(const nsAString& aRepeatCountSpec); + nsresult SetRepeatDur(const nsAString& aRepeatDurSpec); + nsresult SetFillMode(const nsAString& aFillModeSpec); + + void UnsetBeginSpec(RemovalTestFunction aRemove); + void UnsetEndSpec(RemovalTestFunction aRemove); + void UnsetSimpleDuration(); + void UnsetMin(); + void UnsetMax(); + void UnsetRestart(); + void UnsetRepeatCount(); + void UnsetRepeatDur(); + void UnsetFillMode(); + + nsresult SetBeginOrEndSpec(const nsAString& aSpec, + Element* aContextNode, + bool aIsBegin, + RemovalTestFunction aRemove); + void ClearSpecs(TimeValueSpecList& aSpecs, + InstanceTimeList& aInstances, + RemovalTestFunction aRemove); + void ClearIntervals(); + void DoSampleAt(nsSMILTime aContainerTime, bool aEndOnly); + + /** + * Helper function to check for an early end and, if necessary, update the + * current interval accordingly. + * + * See SMIL 3.0, section 5.4.5, Element life cycle, "Active Time - Playing an + * interval" for a description of ending early. + * + * @param aSampleTime The current sample time. Early ends should only be + * applied at the last possible moment (i.e. if they are at + * or before the current sample time) and only if the + * current interval is not already ending. + * @return true if the end time of the current interval was updated, + * false otherwise. + */ + bool ApplyEarlyEnd(const nsSMILTimeValue& aSampleTime); + + /** + * Clears certain state in response to the element restarting. + * + * This state is described in SMIL 3.0, section 5.4.3, Resetting element state + */ + void Reset(); + + /** + * Clears all accumulated timing state except for those instance times for + * which aRemove does not return true. + * + * Unlike the Reset method which only clears instance times, this clears the + * element's state, intervals (including current interval), and tells the + * client animation function to stop applying a result. In effect, it returns + * the element to its initial state but preserves any instance times excluded + * by the passed-in function. + */ + void ClearTimingState(RemovalTestFunction aRemove); + + /** + * Recreates timing state by re-applying begin/end attributes specified on + * the associated animation element. + * + * Note that this does not completely restore the information cleared by + * ClearTimingState since it leaves the element in the startup state. + * The element state will be updated on the next sample. + */ + void RebuildTimingState(RemovalTestFunction aRemove); + + /** + * Completes a seek operation by sending appropriate events and, in the case + * of a backwards seek, updating the state of timing information that was + * previously considered historical. + */ + void DoPostSeek(); + + /** + * Unmarks instance times that were previously preserved because they were + * considered important historical milestones but are no longer such because + * a backwards seek has been performed. + */ + void UnpreserveInstanceTimes(InstanceTimeList& aList); + + /** + * Helper function to iterate through this element's accumulated timing + * information (specifically old nsSMILIntervals and nsSMILTimeInstanceTimes) + * and discard items that are no longer needed or exceed some threshold of + * accumulated state. + */ + void FilterHistory(); + + // Helper functions for FilterHistory to clear old nsSMILIntervals and + // nsSMILInstanceTimes respectively. + void FilterIntervals(); + void FilterInstanceTimes(InstanceTimeList& aList); + + /** + * Calculates the next acceptable interval for this element after the + * specified interval, or, if no previous interval is specified, it will be + * the first interval with an end time after t=0. + * + * @see SMILANIM 3.6.8 + * + * @param aPrevInterval The previous interval used. If supplied, the first + * interval that begins after aPrevInterval will be + * returned. May be nullptr. + * @param aReplacedInterval The interval that is being updated (if any). This + * used to ensure we don't return interval endpoints + * that are dependent on themselves. May be nullptr. + * @param aFixedBeginTime The time to use for the start of the interval. This + * is used when only the endpoint of the interval + * should be updated such as when the animation is in + * the ACTIVE state. May be nullptr. + * @param[out] aResult The next interval. Will be unchanged if no suitable + * interval was found (in which case false will be + * returned). + * @return true if a suitable interval was found, false otherwise. + */ + bool GetNextInterval(const nsSMILInterval* aPrevInterval, + const nsSMILInterval* aReplacedInterval, + const nsSMILInstanceTime* aFixedBeginTime, + nsSMILInterval& aResult) const; + nsSMILInstanceTime* GetNextGreater(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const; + nsSMILInstanceTime* GetNextGreaterOrEqual(const InstanceTimeList& aList, + const nsSMILTimeValue& aBase, + int32_t& aPosition) const; + nsSMILTimeValue CalcActiveEnd(const nsSMILTimeValue& aBegin, + const nsSMILTimeValue& aEnd) const; + nsSMILTimeValue GetRepeatDuration() const; + nsSMILTimeValue ApplyMinAndMax(const nsSMILTimeValue& aDuration) const; + nsSMILTime ActiveTimeToSimpleTime(nsSMILTime aActiveTime, + uint32_t& aRepeatIteration); + nsSMILInstanceTime* CheckForEarlyEnd( + const nsSMILTimeValue& aContainerTime) const; + void UpdateCurrentInterval(bool aForceChangeNotice = false); + void SampleSimpleTime(nsSMILTime aActiveTime); + void SampleFillValue(); + nsresult AddInstanceTimeFromCurrentTime(nsSMILTime aCurrentTime, + double aOffsetSeconds, bool aIsBegin); + void RegisterMilestone(); + bool GetNextMilestone(nsSMILMilestone& aNextMilestone) const; + + // Notification methods. Note that these notifications can result in nested + // calls to this same object. Therefore, + // (i) we should not perform notification until this object is in + // a consistent state to receive callbacks, and + // (ii) after calling these methods we must assume that the state of the + // element may have changed. + void NotifyNewInterval(); + void NotifyChangedInterval(nsSMILInterval* aInterval, + bool aBeginObjectChanged, + bool aEndObjectChanged); + + void FireTimeEventAsync(mozilla::EventMessage aMsg, + int32_t aDetail); + const nsSMILInstanceTime* GetEffectiveBeginInstance() const; + const nsSMILInterval* GetPreviousInterval() const; + bool HasPlayed() const { return !mOldIntervals.IsEmpty(); } + bool HasClientInFillRange() const; + bool EndHasEventConditions() const; + bool AreEndTimesDependentOn( + const nsSMILInstanceTime* aBase) const; + + // Reset the current interval by first passing ownership to a temporary + // variable so that if Unlink() results in us receiving a callback, + // mCurrentInterval will be nullptr and we will be in a consistent state. + void ResetCurrentInterval() + { + if (mCurrentInterval) { + // Transfer ownership to temp var. (This sets mCurrentInterval to null.) + nsAutoPtr<nsSMILInterval> interval(mozilla::Move(mCurrentInterval)); + interval->Unlink(); + } + } + + // + // Members + // + mozilla::dom::SVGAnimationElement* mAnimationElement; // [weak] won't outlive + // owner + TimeValueSpecList mBeginSpecs; // [strong] + TimeValueSpecList mEndSpecs; // [strong] + + nsSMILTimeValue mSimpleDur; + + nsSMILRepeatCount mRepeatCount; + nsSMILTimeValue mRepeatDur; + + nsSMILTimeValue mMin; + nsSMILTimeValue mMax; + + enum nsSMILFillMode : uint8_t + { + FILL_REMOVE, + FILL_FREEZE + }; + nsSMILFillMode mFillMode; + static nsAttrValue::EnumTable sFillModeTable[]; + + enum nsSMILRestartMode : uint8_t + { + RESTART_ALWAYS, + RESTART_WHENNOTACTIVE, + RESTART_NEVER + }; + nsSMILRestartMode mRestartMode; + static nsAttrValue::EnumTable sRestartModeTable[]; + + InstanceTimeList mBeginInstances; + InstanceTimeList mEndInstances; + uint32_t mInstanceSerialIndex; + + nsSMILAnimationFunction* mClient; + nsAutoPtr<nsSMILInterval> mCurrentInterval; + IntervalList mOldIntervals; + uint32_t mCurrentRepeatIteration; + nsSMILMilestone mPrevRegisteredMilestone; + static const nsSMILMilestone sMaxMilestone; + static const uint8_t sMaxNumIntervals; + static const uint8_t sMaxNumInstanceTimes; + + // Set of dependent time value specs to be notified when establishing a new + // current interval. Change notifications and delete notifications are handled + // by the interval. + // + // [weak] The nsSMILTimeValueSpec objects register themselves and unregister + // on destruction. Likewise, we notify them when we are destroyed. + TimeValueSpecHashSet mTimeDependents; + + /** + * The state of the element in its life-cycle. These states are based on the + * element life-cycle described in SMILANIM 3.6.8 + */ + enum nsSMILElementState + { + STATE_STARTUP, + STATE_WAITING, + STATE_ACTIVE, + STATE_POSTACTIVE + }; + nsSMILElementState mElementState; + + enum nsSMILSeekState + { + SEEK_NOT_SEEKING, + SEEK_FORWARD_FROM_ACTIVE, + SEEK_FORWARD_FROM_INACTIVE, + SEEK_BACKWARD_FROM_ACTIVE, + SEEK_BACKWARD_FROM_INACTIVE + }; + nsSMILSeekState mSeekState; + + // Used to batch updates to the timing model + class AutoIntervalUpdateBatcher; + bool mDeferIntervalUpdates; + bool mDoDeferredUpdate; // Set if an update to the current interval was + // requested while mDeferIntervalUpdates was set + bool mIsDisabled; + + // Stack-based helper class to call UpdateCurrentInterval when it is destroyed + class AutoIntervalUpdater; + + // Recursion depth checking + uint8_t mDeleteCount; + uint8_t mUpdateIntervalRecursionDepth; + static const uint8_t sMaxUpdateIntervalRecursionDepth; +}; + +inline void +ImplCycleCollectionUnlink(nsSMILTimedElement& aField) +{ + aField.Unlink(); +} + +inline void +ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + nsSMILTimedElement& aField, + const char* aName, + uint32_t aFlags = 0) +{ + aField.Traverse(&aCallback); +} + +#endif // NS_SMILTIMEDELEMENT_H_ diff --git a/dom/smil/nsSMILTypes.h b/dom/smil/nsSMILTypes.h new file mode 100644 index 000000000..82153c867 --- /dev/null +++ b/dom/smil/nsSMILTypes.h @@ -0,0 +1,26 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILTYPES_H_ +#define NS_SMILTYPES_H_ + +#include <stdint.h> + +// A timestamp in milliseconds +// +// A time may represent: +// +// simple time -- offset within the simple duration +// active time -- offset within the active duration +// document time -- offset since the document begin +// wallclock time -- "real" time -- offset since the epoch +// +// For an overview of how this class is related to other SMIL time classes see +// the documentstation in nsSMILTimeValue.h +// +typedef int64_t nsSMILTime; + +#endif // NS_SMILTYPES_H_ diff --git a/dom/smil/nsSMILValue.cpp b/dom/smil/nsSMILValue.cpp new file mode 100644 index 000000000..cd881b0b0 --- /dev/null +++ b/dom/smil/nsSMILValue.cpp @@ -0,0 +1,162 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "nsSMILValue.h" +#include "nsDebug.h" +#include <string.h> + +//---------------------------------------------------------------------- +// Public methods + +nsSMILValue::nsSMILValue(const nsISMILType* aType) + : mType(nsSMILNullType::Singleton()) +{ + if (!aType) { + NS_ERROR("Trying to construct nsSMILValue with null mType pointer"); + return; + } + + InitAndCheckPostcondition(aType); +} + +nsSMILValue::nsSMILValue(const nsSMILValue& aVal) + : mType(nsSMILNullType::Singleton()) +{ + InitAndCheckPostcondition(aVal.mType); + mType->Assign(*this, aVal); +} + +const nsSMILValue& +nsSMILValue::operator=(const nsSMILValue& aVal) +{ + if (&aVal == this) + return *this; + + if (mType != aVal.mType) { + DestroyAndReinit(aVal.mType); + } + + mType->Assign(*this, aVal); + + return *this; +} + +// Move constructor / reassignment operator: +nsSMILValue::nsSMILValue(nsSMILValue&& aVal) + : mU(aVal.mU), // Copying union is only OK because we clear aVal.mType below. + mType(aVal.mType) +{ + // Leave aVal with a null type, so that it's safely destructible (and won't + // mess with anything referenced by its union, which we've copied). + aVal.mType = nsSMILNullType::Singleton(); +} + +nsSMILValue& +nsSMILValue::operator=(nsSMILValue&& aVal) +{ + if (!IsNull()) { + // Clean up any data we're currently tracking. + DestroyAndCheckPostcondition(); + } + + // Copy the union (which could include a pointer to external memory) & mType: + mU = aVal.mU; + mType = aVal.mType; + + // Leave aVal with a null type, so that it's safely destructible (and won't + // mess with anything referenced by its union, which we've now copied). + aVal.mType = nsSMILNullType::Singleton(); + + return *this; +} + +bool +nsSMILValue::operator==(const nsSMILValue& aVal) const +{ + if (&aVal == this) + return true; + + return mType == aVal.mType && mType->IsEqual(*this, aVal); +} + +nsresult +nsSMILValue::Add(const nsSMILValue& aValueToAdd, uint32_t aCount) +{ + if (aValueToAdd.mType != mType) { + NS_ERROR("Trying to add incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->Add(*this, aValueToAdd, aCount); +} + +nsresult +nsSMILValue::SandwichAdd(const nsSMILValue& aValueToAdd) +{ + if (aValueToAdd.mType != mType) { + NS_ERROR("Trying to add incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->SandwichAdd(*this, aValueToAdd); +} + +nsresult +nsSMILValue::ComputeDistance(const nsSMILValue& aTo, double& aDistance) const +{ + if (aTo.mType != mType) { + NS_ERROR("Trying to calculate distance between incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->ComputeDistance(*this, aTo, aDistance); +} + +nsresult +nsSMILValue::Interpolate(const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const +{ + if (aEndVal.mType != mType) { + NS_ERROR("Trying to interpolate between incompatible types"); + return NS_ERROR_FAILURE; + } + + if (aResult.mType != mType) { + // Outparam has wrong type + aResult.DestroyAndReinit(mType); + } + + return mType->Interpolate(*this, aEndVal, aUnitDistance, aResult); +} + +//---------------------------------------------------------------------- +// Helper methods + +// Wrappers for nsISMILType::Init & ::Destroy that verify their postconditions +void +nsSMILValue::InitAndCheckPostcondition(const nsISMILType* aNewType) +{ + aNewType->Init(*this); + MOZ_ASSERT(mType == aNewType, + "Post-condition of Init failed. nsSMILValue is invalid"); +} + +void +nsSMILValue::DestroyAndCheckPostcondition() +{ + mType->Destroy(*this); + MOZ_ASSERT(IsNull(), + "Post-condition of Destroy failed. " + "nsSMILValue not null after destroying"); +} + +void +nsSMILValue::DestroyAndReinit(const nsISMILType* aNewType) +{ + DestroyAndCheckPostcondition(); + InitAndCheckPostcondition(aNewType); +} diff --git a/dom/smil/nsSMILValue.h b/dom/smil/nsSMILValue.h new file mode 100644 index 000000000..c0998d61d --- /dev/null +++ b/dom/smil/nsSMILValue.h @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef NS_SMILVALUE_H_ +#define NS_SMILVALUE_H_ + +#include "nsISMILType.h" +#include "nsSMILNullType.h" + +/** + * Although objects of this type are generally only created on the stack and + * only exist during the taking of a new time sample, that's not always the + * case. The nsSMILValue objects obtained from attributes' base values are + * cached so that the SMIL engine can make certain optimizations during a + * sample if the base value has not changed since the last sample (potentially + * avoiding recomposing). These nsSMILValue objects typically live much longer + * than a single sample. + */ +class nsSMILValue +{ +public: + nsSMILValue() : mU(), mType(nsSMILNullType::Singleton()) { } + explicit nsSMILValue(const nsISMILType* aType); + nsSMILValue(const nsSMILValue& aVal); + + ~nsSMILValue() + { + mType->Destroy(*this); + } + + const nsSMILValue& operator=(const nsSMILValue& aVal); + + // Move constructor / reassignment operator: + nsSMILValue(nsSMILValue&& aVal); + nsSMILValue& operator=(nsSMILValue&& aVal); + + // Equality operators. These are allowed to be conservative (return false + // more than you'd expect) - see comment above nsISMILType::IsEqual. + bool operator==(const nsSMILValue& aVal) const; + bool operator!=(const nsSMILValue& aVal) const { + return !(*this == aVal); + } + + bool IsNull() const + { + return (mType == nsSMILNullType::Singleton()); + } + + nsresult Add(const nsSMILValue& aValueToAdd, uint32_t aCount = 1); + nsresult SandwichAdd(const nsSMILValue& aValueToAdd); + nsresult ComputeDistance(const nsSMILValue& aTo, double& aDistance) const; + nsresult Interpolate(const nsSMILValue& aEndVal, + double aUnitDistance, + nsSMILValue& aResult) const; + + union { + bool mBool; + uint64_t mUint; + int64_t mInt; + double mDouble; + struct { + float mAngle; + uint16_t mUnit; + uint16_t mOrientType; + } mOrient; + int32_t mIntPair[2]; + float mNumberPair[2]; + void* mPtr; + } mU; + const nsISMILType* mType; + +protected: + void InitAndCheckPostcondition(const nsISMILType* aNewType); + void DestroyAndCheckPostcondition(); + void DestroyAndReinit(const nsISMILType* aNewType); +}; + +#endif // NS_SMILVALUE_H_ diff --git a/dom/smil/test/db_smilAnimateMotion.js b/dom/smil/test/db_smilAnimateMotion.js new file mode 100644 index 000000000..c4dfb4e24 --- /dev/null +++ b/dom/smil/test/db_smilAnimateMotion.js @@ -0,0 +1,253 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* testcase data for <animateMotion> */ + +// Fake motion 'attribute', to satisfy testing code that expects an attribute. +var gMotionAttr = new AdditiveAttribute(SMILUtil.getMotionFakeAttributeName(), + "XML", "rect"); + +// CTM-summary-definitions, for re-use by multiple testcase bundles below. +var _reusedCTMLists = { + pacedBasic: { ctm0: [100, 200, 0], + ctm1_6: [105, 205, 0], + ctm1_3: [110, 210, 0], + ctm2_3: [120, 220, 0], + ctm1: [130, 210, 0] + }, + pacedR60: { ctm0: [100, 200, Math.PI/3], + ctm1_6: [105, 205, Math.PI/3], + ctm1_3: [110, 210, Math.PI/3], + ctm2_3: [120, 220, Math.PI/3], + ctm1: [130, 210, Math.PI/3] + }, + pacedRAuto: { ctm0: [100, 200, Math.PI/4], + ctm1_6: [105, 205, Math.PI/4], + ctm1_3: [110, 210, Math.PI/4], + ctm2_3: [120, 220, -Math.PI/4], + ctm1: [130, 210, -Math.PI/4] + }, + pacedRAutoReverse : { ctm0: [100, 200, 5*Math.PI/4], + ctm1_6: [105, 205, 5*Math.PI/4], + ctm1_3: [110, 210, 5*Math.PI/4], + ctm2_3: [120, 220, 3*Math.PI/4], + ctm1: [130, 210, 3*Math.PI/4] + }, + + discreteBasic : { ctm0: [100, 200, 0], + ctm1_6: [100, 200, 0], + ctm1_3: [120, 220, 0], + ctm2_3: [130, 210, 0], + ctm1: [130, 210, 0] + }, + discreteRAuto : { ctm0: [100, 200, Math.PI/4], + ctm1_6: [100, 200, Math.PI/4], + ctm1_3: [120, 220, -Math.PI/4], + ctm2_3: [130, 210, -Math.PI/4], + ctm1: [130, 210, -Math.PI/4] + }, + justMoveBasic : { ctm0: [40, 80, 0], + ctm1_6: [40, 80, 0], + ctm1_3: [40, 80, 0], + ctm2_3: [40, 80, 0], + ctm1: [40, 80, 0] + }, + justMoveR60 : { ctm0: [40, 80, Math.PI/3], + ctm1_6: [40, 80, Math.PI/3], + ctm1_3: [40, 80, Math.PI/3], + ctm2_3: [40, 80, Math.PI/3], + ctm1: [40, 80, Math.PI/3] + }, + justMoveRAuto : { ctm0: [40, 80, Math.atan(2)], + ctm1_6: [40, 80, Math.atan(2)], + ctm1_3: [40, 80, Math.atan(2)], + ctm2_3: [40, 80, Math.atan(2)], + ctm1: [40, 80, Math.atan(2)] + }, + justMoveRAutoReverse : { ctm0: [40, 80, Math.PI + Math.atan(2)], + ctm1_6: [40, 80, Math.PI + Math.atan(2)], + ctm1_3: [40, 80, Math.PI + Math.atan(2)], + ctm2_3: [40, 80, Math.PI + Math.atan(2)], + ctm1: [40, 80, Math.PI + Math.atan(2)] + }, + nullMoveBasic : { ctm0: [0, 0, 0], + ctm1_6: [0, 0, 0], + ctm1_3: [0, 0, 0], + ctm2_3: [0, 0, 0], + ctm1: [0, 0, 0] + }, + nullMoveRAutoReverse : { ctm0: [0, 0, Math.PI], + ctm1_6: [0, 0, Math.PI], + ctm1_3: [0, 0, Math.PI], + ctm2_3: [0, 0, Math.PI], + ctm1: [0, 0, Math.PI] + }, +}; + +var gMotionBundles = +[ + // Bundle to test basic functionality (using default calcMode='paced') + new TestcaseBundle(gMotionAttr, [ + // Basic paced-mode (default) test, with values/mpath/path + new AnimMotionTestcase({ "values": "100, 200; 120, 220; 130, 210" }, + _reusedCTMLists.pacedBasic), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210" }, + _reusedCTMLists.pacedBasic), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210" }, + _reusedCTMLists.pacedBasic), + + // ..and now with rotate=constant value in degrees + new AnimMotionTestcase({ "values": "100,200; 120,220; 130, 210", + "rotate": "60" }, + _reusedCTMLists.pacedR60), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "rotate": "60" }, + _reusedCTMLists.pacedR60), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "rotate": "60" }, + _reusedCTMLists.pacedR60), + + // ..and now with rotate=constant value in radians + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "rotate": "1.0471975512rad" }, // pi/3 + _reusedCTMLists.pacedR60), + + // ..and now with rotate=auto + new AnimMotionTestcase({ "values": "100,200; 120,220; 130, 210", + "rotate": "auto" }, + _reusedCTMLists.pacedRAuto), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "rotate": "auto" }, + _reusedCTMLists.pacedRAuto), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "rotate": "auto" }, + _reusedCTMLists.pacedRAuto), + + // ..and now with rotate=auto-reverse + new AnimMotionTestcase({ "values": "100,200; 120,220; 130, 210", + "rotate": "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "rotate": "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "rotate": "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse), + + ]), + + // Bundle to test calcMode='discrete' + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "100, 200; 120, 220; 130, 210", + "calcMode": "discrete" }, + _reusedCTMLists.discreteBasic), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "calcMode": "discrete" }, + _reusedCTMLists.discreteBasic), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "calcMode": "discrete" }, + _reusedCTMLists.discreteBasic), + // ..and now with rotate=auto + new AnimMotionTestcase({ "values": "100, 200; 120, 220; 130, 210", + "calcMode": "discrete", + "rotate": "auto" }, + _reusedCTMLists.discreteRAuto), + new AnimMotionTestcase({ "path": "M100 200 L120 220 L130 210", + "calcMode": "discrete", + "rotate": "auto" }, + _reusedCTMLists.discreteRAuto), + new AnimMotionTestcase({ "mpath": "M100 200 L120 220 L130 210", + "calcMode": "discrete", + "rotate": "auto" }, + _reusedCTMLists.discreteRAuto), + ]), + + // Bundle to test relative units ('em') + new TestcaseBundle(gMotionAttr, [ + // First with unitless values from->by... + new AnimMotionTestcase({ "from": "10, 10", + "by": "30, 60" }, + { ctm0: [10, 10, 0], + ctm1_6: [15, 20, 0], + ctm1_3: [20, 30, 0], + ctm2_3: [30, 50, 0], + ctm1: [40, 70, 0] + }), + // ... then add 'em' units (with 1em=10px) on half the values + new AnimMotionTestcase({ "from": "1em, 10", + "by": "30, 6em" }, + { ctm0: [10, 10, 0], + ctm1_6: [15, 20, 0], + ctm1_3: [20, 30, 0], + ctm2_3: [30, 50, 0], + ctm1: [40, 70, 0] + }), + ]), + + // Bundle to test a path with just a "move" command and nothing else + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "40, 80" }, + _reusedCTMLists.justMoveBasic), + new AnimMotionTestcase({ "path": "M40 80" }, + _reusedCTMLists.justMoveBasic), + new AnimMotionTestcase({ "mpath": "m40 80" }, + _reusedCTMLists.justMoveBasic), + ]), + // ... and now with a fixed rotate-angle + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "40, 80", + "rotate": "60" }, + _reusedCTMLists.justMoveR60), + new AnimMotionTestcase({ "path": "M40 80", + "rotate": "60" }, + _reusedCTMLists.justMoveR60), + new AnimMotionTestcase({ "mpath": "m40 80", + "rotate": "60" }, + _reusedCTMLists.justMoveR60), + ]), + // ... and now with 'auto' (should use the move itself as + // our tangent angle, I think) + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "40, 80", + "rotate": "auto" }, + _reusedCTMLists.justMoveRAuto), + new AnimMotionTestcase({ "path": "M40 80", + "rotate": "auto" }, + _reusedCTMLists.justMoveRAuto), + new AnimMotionTestcase({ "mpath": "m40 80", + "rotate": "auto" }, + _reusedCTMLists.justMoveRAuto), + ]), + // ... and now with 'auto-reverse' + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "40, 80", + "rotate": "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse), + new AnimMotionTestcase({ "path": "M40 80", + "rotate": "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse), + new AnimMotionTestcase({ "mpath": "m40 80", + "rotate": "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse), + ]), + // ... and now with a null move to make sure 'auto'/'auto-reverse' don't + // blow up + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "0, 0", + "rotate": "auto" }, + _reusedCTMLists.nullMoveBasic), + ]), + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ "values": "0, 0", + "rotate": "auto-reverse" }, + _reusedCTMLists.nullMoveRAutoReverse), + ]), +]; + +// XXXdholbert Add more tests: +// - keyPoints/keyTimes +// - paths with curves +// - Control path with from/by/to diff --git a/dom/smil/test/db_smilCSSFromBy.js b/dom/smil/test/db_smilCSSFromBy.js new file mode 100644 index 000000000..f8b36e70a --- /dev/null +++ b/dom/smil/test/db_smilCSSFromBy.js @@ -0,0 +1,166 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* testcase data for simple "from-by" animations of CSS properties */ + +// NOTE: This js file requires db_smilCSSPropertyList.js + +// Lists of testcases for re-use across multiple properties of the same type +var _fromByTestLists = +{ + color: [ + new AnimTestcaseFromBy("rgb(10, 20, 30)", "currentColor", + { midComp: "rgb(35, 45, 55)", + toComp: "rgb(60, 70, 80)"}), + new AnimTestcaseFromBy("currentColor", "rgb(30, 20, 10)", + { fromComp: "rgb(50, 50, 50)", + midComp: "rgb(65, 60, 55)", + toComp: "rgb(80, 70, 60)"}), + new AnimTestcaseFromBy("rgba(10, 20, 30, 0.2)", "rgba(50, 50, 50, 1)", + // (rgb(10, 20, 30) * 0.2 * 0.5 + rgb(52, 54, 56) * 1.0 * 0.5) * (1 / 0.6) + { midComp: "rgba(45, 48, 52, 0.6)", + // (rgb(10, 20, 30) * 0.2 + rgb(50, 50, 50) * 1) / 1.0 + toComp: "rgb(52, 54, 56)"}), + // Note: technically, the "from" and "by" values in the test case below + // would overflow the maxium color-channel values when added together. + // (e.g. for red [ignoring alpha for now], 100 + 240 = 340 which is > 255) + // The SVG Animation spec says we should clamp color values "as late as + // possible," i.e. allow the channel overflow and clamp at paint-time. + // But for now, we instead clamp the implicit "to" value for the animation + // and interpolate up to that clamped result. + new AnimTestcaseFromBy("rgba(100, 100, 100, 0.6)", "rgba(240, 240, 240, 1)", + // (rgb(100, 100, 100) * 0.6 * 0.5 + rgb(255, 255, 255) * 1.0 * 0.5) * (1 / 0.8) + { midComp: "rgba(197, 197, 197, 0.8)", + // (rgb(100, 100, 100) * 0.6 + rgb(240, 240, 240) is overflowed + toComp: "rgb(255, 255, 255)"}), + ], + lengthNoUnits: [ + new AnimTestcaseFromBy("0", "50", { fromComp: "0px", // 0 acts like 0px + midComp: "25px", + toComp: "50px"}), + new AnimTestcaseFromBy("30", "10", { fromComp: "30px", + midComp: "35px", + toComp: "40px"}), + ], + lengthNoUnitsSVG: [ + new AnimTestcaseFromBy("0", "50", { fromComp: "0", + midComp: "25", + toComp: "50"}), + new AnimTestcaseFromBy("30", "10", { fromComp: "30", + midComp: "35", + toComp: "40"}), + ], + lengthPx: [ + new AnimTestcaseFromBy("0px", "8px", { fromComp: "0px", + midComp: "4px", + toComp: "8px"}), + new AnimTestcaseFromBy("1px", "10px", { midComp: "6px", toComp: "11px"}), + ], + lengthPxSVG: [ + new AnimTestcaseFromBy("0px", "8px", { fromComp: "0", + midComp: "4", + toComp: "8"}), + new AnimTestcaseFromBy("1px", "10px", { fromComp: "1", + midComp: "6", + toComp: "11"}), + ], + opacity: [ + new AnimTestcaseFromBy("1", "-1", { midComp: "0.5", toComp: "0"}), + new AnimTestcaseFromBy("0.4", "-0.6", { midComp: "0.1", toComp: "0"}), + new AnimTestcaseFromBy("0.8", "-1.4", { midComp: "0.1", toComp: "0"}, + "opacities with abs val >1 get clamped too early"), + new AnimTestcaseFromBy("1.2", "-0.6", { midComp: "0.9", toComp: "0.6"}, + "opacities with abs val >1 get clamped too early"), + ], + paint: [ + // The "none" keyword & URI values aren't addiditve, so the animations in + // these testcases are expected to have no effect. + new AnimTestcaseFromBy("none", "none", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "url(#gradB)", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "url(#gradB) red", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "none", { noEffect: 1 }), + new AnimTestcaseFromBy("red", "url(#gradA)", { noEffect: 1 }), + ], + URIsAndNone: [ + // No need to specify { noEffect: 1 }, since plain URI-valued properties + // aren't additive + new AnimTestcaseFromBy("url(#idA)", "url(#idB)"), + new AnimTestcaseFromBy("none", "url(#idB)"), + new AnimTestcaseFromBy("url(#idB)", "inherit"), + ], +}; + +// List of attribute/testcase-list bundles to be tested +var gFromByBundles = +[ + new TestcaseBundle(gPropList.clip, [ + new AnimTestcaseFromBy("rect(1px, 2px, 3px, 4px)", + "rect(10px, 20px, 30px, 40px)", + { midComp: "rect(6px, 12px, 18px, 24px)", + toComp: "rect(11px, 22px, 33px, 44px)"}), + // Adding "auto" (either as a standalone value or a subcomponent value) + // should cause animation to fail. + new AnimTestcaseFromBy("auto", "auto", { noEffect: 1 }), + new AnimTestcaseFromBy("auto", + "rect(auto, auto, auto, auto)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(auto, auto, auto, auto)", + "rect(auto, auto, auto, auto)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(1px, 2px, 3px, 4px)", "auto", { noEffect: 1 }), + new AnimTestcaseFromBy("auto", "rect(1px, 2px, 3px, 4px)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(1px, 2px, 3px, auto)", + "rect(10px, 20px, 30px, 40px)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(1px, auto, 3px, 4px)", + "rect(10px, auto, 30px, 40px)", { noEffect: 1 }), + new AnimTestcaseFromBy("rect(1px, 2px, 3px, 4px)", + "rect(10px, auto, 30px, 40px)", { noEffect: 1 }), + ]), + // Check that 'by' animations for 'cursor' has no effect + new TestcaseBundle(gPropList.cursor, [ + new AnimTestcaseFromBy("crosshair", "move"), + ]), + new TestcaseBundle(gPropList.fill, [].concat(_fromByTestLists.color, + _fromByTestLists.paint)), + // Check that 'by' animations involving URIs have no effect + new TestcaseBundle(gPropList.filter, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.font, [ + new AnimTestcaseFromBy("10px serif", + "normal normal 400 100px / 10px monospace"), + ]), + new TestcaseBundle(gPropList.font_size, + [].concat(_fromByTestLists.lengthNoUnits, + _fromByTestLists.lengthPx)), + new TestcaseBundle(gPropList.font_size_adjust, [ + // These testcases implicitly have no effect, because font-size-adjust is + // non-additive (and is declared as such in db_smilCSSPropertyList.js) + new AnimTestcaseFromBy("0.5", "0.1"), + new AnimTestcaseFromBy("none", "0.1"), + new AnimTestcaseFromBy("0.1", "none") + ]), + new TestcaseBundle(gPropList.lighting_color, _fromByTestLists.color), + new TestcaseBundle(gPropList.marker, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_end, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_mid, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_start, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.overflow, [ + new AnimTestcaseFromBy("inherit", "auto"), + new AnimTestcaseFromBy("scroll", "hidden") + ]), + new TestcaseBundle(gPropList.opacity, _fromByTestLists.opacity), + new TestcaseBundle(gPropList.stroke_miterlimit, [ + new AnimTestcaseFromBy("1", "1", { midComp: "1.5", toComp: "2" }), + new AnimTestcaseFromBy("20.1", "-10", { midComp: "15.1", toComp: "10.1" }), + ]), + new TestcaseBundle(gPropList.stroke_dasharray, [ + // These testcases implicitly have no effect, because stroke-dasharray is + // non-additive (and is declared as such in db_smilCSSPropertyList.js) + new AnimTestcaseFromBy("none", "5"), + new AnimTestcaseFromBy("10", "5"), + new AnimTestcaseFromBy("1", "2, 3"), + ]), + new TestcaseBundle(gPropList.stroke_width, + [].concat(_fromByTestLists.lengthNoUnitsSVG, + _fromByTestLists.lengthPxSVG)) +]; diff --git a/dom/smil/test/db_smilCSSFromTo.js b/dom/smil/test/db_smilCSSFromTo.js new file mode 100644 index 000000000..fe9cecd6c --- /dev/null +++ b/dom/smil/test/db_smilCSSFromTo.js @@ -0,0 +1,483 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* testcase data for simple "from-to" animations of CSS properties */ + +// NOTE: This js file requires db_smilCSSPropertyList.js + +// NOTE: I'm Including 'inherit' and 'currentColor' as interpolatable values. +// According to SVG Mobile 1.2 section 16.2.9, "keywords such as inherit which +// yield a numeric computed value may be included in the values list for an +// interpolated animation". + +// Path of test URL (stripping off final slash + filename), for use in +// generating computed value of 'cursor' property +var _testPath = document.URL.substring(0, document.URL.lastIndexOf('/')); + +// Lists of testcases for re-use across multiple properties of the same type +var _fromToTestLists = { + color: [ + new AnimTestcaseFromTo("rgb(100, 100, 100)", "rgb(200, 200, 200)", + { midComp: "rgb(150, 150, 150)" }), + new AnimTestcaseFromTo("#F02000", "#0080A0", + { fromComp: "rgb(240, 32, 0)", + midComp: "rgb(120, 80, 80)", + toComp: "rgb(0, 128, 160)" }), + new AnimTestcaseFromTo("crimson", "lawngreen", + { fromComp: "rgb(220, 20, 60)", + midComp: "rgb(172, 136, 30)", + toComp: "rgb(124, 252, 0)" }), + new AnimTestcaseFromTo("currentColor", "rgb(100, 100, 100)", + { fromComp: "rgb(50, 50, 50)", + midComp: "rgb(75, 75, 75)" }), + new AnimTestcaseFromTo("rgba(10, 20, 30, 0.2)", "rgba(50, 50, 50, 1)", + // (rgb(10, 20, 30) * 0.2 * 0.5 + rgb(50, 50, 50) * 1.0 * 0.5) * (1 / 0.6) + { midComp: "rgba(43, 45, 47, 0.6)", + toComp: "rgb(50, 50, 50)"}), + ], + colorFromInheritBlack: [ + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", + { fromComp: "rgb(0, 0, 0)", + midComp: "rgb(100, 100, 100)" }), + ], + colorFromInheritWhite: [ + new AnimTestcaseFromTo("inherit", "rgb(205, 205, 205)", + { fromComp: "rgb(255, 255, 255)", + midComp: "rgb(230, 230, 230)" }), + ], + paintServer: [ + new AnimTestcaseFromTo("none", "none"), + new AnimTestcaseFromTo("none", "blue", { toComp : "rgb(0, 0, 255)" }), + new AnimTestcaseFromTo("rgb(50, 50, 50)", "none"), + new AnimTestcaseFromTo("url(#gradA)", "url(#gradB) currentColor", + { fromComp: "url(\"" + document.URL + + "#gradA\") rgb(0, 0, 0)", + toComp: "url(\"" + document.URL + + "#gradB\") rgb(50, 50, 50)" }, + "need support for URI-based paints"), + new AnimTestcaseFromTo("url(#gradA) orange", "url(#gradB)", + { fromComp: "url(\"" + document.URL + + "#gradA\") rgb(255, 165, 0)", + toComp: "url(\"" + document.URL + + "#gradB\") rgb(0, 0, 0)" }, + "need support for URI-based paints"), + new AnimTestcaseFromTo("url(#no_grad)", "url(#gradB)", + { fromComp: "url(\"" + document.URL + + "#no_grad\") " + "rgb(0, 0, 0)", + toComp: "url(\"" + document.URL + + "#gradB\") rgb(0, 0, 0)" }, + "need support for URI-based paints"), + new AnimTestcaseFromTo("url(#no_grad) rgb(1,2,3)", "url(#gradB) blue", + { fromComp: "url(\"" + document.URL + + "#no_grad\") " + "rgb(1, 2, 3)", + toComp: "url(\"" + document.URL + + "#gradB\") rgb(0, 0, 255)" }, + "need support for URI-based paints"), + ], + lengthNoUnits: [ + new AnimTestcaseFromTo("0", "20", { fromComp: "0px", + midComp: "10px", + toComp: "20px"}), + new AnimTestcaseFromTo("50", "0", { fromComp: "50px", + midComp: "25px", + toComp: "0px"}), + new AnimTestcaseFromTo("30", "80", { fromComp: "30px", + midComp: "55px", + toComp: "80px"}), + ], + lengthNoUnitsSVG: [ + new AnimTestcaseFromTo("0", "20", { fromComp: "0", + midComp: "10", + toComp: "20"}), + new AnimTestcaseFromTo("50", "0", { fromComp: "50", + midComp: "25", + toComp: "0"}), + new AnimTestcaseFromTo("30", "80", { fromComp: "30", + midComp: "55", + toComp: "80"}), + ], + lengthPx: [ + new AnimTestcaseFromTo("0px", "12px", { fromComp: "0px", + midComp: "6px"}), + new AnimTestcaseFromTo("16px", "0px", { midComp: "8px", + toComp: "0px"}), + new AnimTestcaseFromTo("10px", "20px", { midComp: "15px"}), + new AnimTestcaseFromTo("41px", "1px", { midComp: "21px"}), + ], + lengthPxSVG: [ + new AnimTestcaseFromTo("0px", "12px", { fromComp: "0", + midComp: "6", + toComp: "12"}), + new AnimTestcaseFromTo("16px", "0px", { fromComp: "16", + midComp: "8", + toComp: "0"}), + new AnimTestcaseFromTo("10px", "20px", { fromComp: "10", + midComp: "15", + toComp: "20"}), + new AnimTestcaseFromTo("41px", "1px", { fromComp: "41", + midComp: "21", + toComp: "1"}), + ], + lengthPctSVG: [ + new AnimTestcaseFromTo("20.5%", "0.5%", { midComp: "10.5%" }), + ], + lengthPxPctSVG: [ + new AnimTestcaseFromTo("10px", "10%", { midComp: "15px"}, + "need support for interpolating between " + + "px and percent values"), + ], + lengthPxNoUnitsSVG: [ + new AnimTestcaseFromTo("10", "20px", { fromComp: "10", + midComp: "15", + toComp: "20"}), + new AnimTestcaseFromTo("10px", "20", { fromComp: "10", + midComp: "15", + toComp: "20"}), + ], + opacity: [ + new AnimTestcaseFromTo("1", "0", { midComp: "0.5" }), + new AnimTestcaseFromTo("0.2", "0.12", { midComp: "0.16" }), + new AnimTestcaseFromTo("0.5", "0.7", { midComp: "0.6" }), + new AnimTestcaseFromTo("0.5", "inherit", + { midComp: "0.75", toComp: "1" }), + // Make sure we don't clamp out-of-range values before interpolation + new AnimTestcaseFromTo("0.2", "1.2", + { midComp: "0.7", toComp: "1" }, + "opacities with abs val >1 get clamped too early"), + new AnimTestcaseFromTo("-0.2", "0.6", + { fromComp: "0", midComp: "0.2" }), + new AnimTestcaseFromTo("-1.2", "1.6", + { fromComp: "0", midComp: "0.2", toComp: "1" }, + "opacities with abs val >1 get clamped too early"), + new AnimTestcaseFromTo("-0.6", "1.4", + { fromComp: "0", midComp: "0.4", toComp: "1" }, + "opacities with abs val >1 get clamped too early"), + ], + URIsAndNone: [ + new AnimTestcaseFromTo("url(#idA)", "url(#idB)", + { fromComp: "url(\"#idA\")", + toComp: "url(\"#idB\")"}), + new AnimTestcaseFromTo("none", "url(#idB)", + { toComp: "url(\"#idB\")"}), + new AnimTestcaseFromTo("url(#idB)", "inherit", + { fromComp: "url(\"#idB\")", + toComp: "none"}), + ], +}; + +// List of attribute/testcase-list bundles to be tested +var gFromToBundles = [ + new TestcaseBundle(gPropList.clip, [ + new AnimTestcaseFromTo("rect(1px, 2px, 3px, 4px)", + "rect(11px, 22px, 33px, 44px)", + { midComp: "rect(6px, 12px, 18px, 24px)" }), + new AnimTestcaseFromTo("rect(1px, auto, 3px, 4px)", + "rect(11px, auto, 33px, 44px)", + { midComp: "rect(6px, auto, 18px, 24px)" }), + new AnimTestcaseFromTo("auto", "auto"), + new AnimTestcaseFromTo("rect(auto, auto, auto, auto)", + "rect(auto, auto, auto, auto)"), + // Interpolation not supported in these next cases (with auto --> px-value) + new AnimTestcaseFromTo("rect(1px, auto, 3px, auto)", + "rect(11px, auto, 33px, 44px)"), + new AnimTestcaseFromTo("rect(1px, 2px, 3px, 4px)", + "rect(11px, auto, 33px, 44px)"), + new AnimTestcaseFromTo("rect(1px, 2px, 3px, 4px)", "auto"), + new AnimTestcaseFromTo("auto", "rect(1px, 2px, 3px, 4px)"), + ]), + new TestcaseBundle(gPropList.clip_path, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.clip_rule, [ + new AnimTestcaseFromTo("nonzero", "evenodd"), + new AnimTestcaseFromTo("evenodd", "inherit", { toComp: "nonzero" }), + ]), + new TestcaseBundle(gPropList.color, + [].concat(_fromToTestLists.color, [ + // Note: inherited value is rgb(50, 50, 50) (set on <svg>) + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", + { fromComp: "rgb(50, 50, 50)", + midComp: "rgb(125, 125, 125)" }), + ])), + new TestcaseBundle(gPropList.color_interpolation, [ + new AnimTestcaseFromTo("sRGB", "auto", { fromComp: "srgb" }), + new AnimTestcaseFromTo("inherit", "linearRGB", + { fromComp: "srgb", toComp: "linearrgb" }), + ]), + new TestcaseBundle(gPropList.color_interpolation_filters, [ + new AnimTestcaseFromTo("sRGB", "auto", { fromComp: "srgb" }), + new AnimTestcaseFromTo("auto", "inherit", + { toComp: "linearrgb" }), + ]), + new TestcaseBundle(gPropList.cursor, [ + new AnimTestcaseFromTo("crosshair", "move"), + new AnimTestcaseFromTo("url('a.cur'), url('b.cur'), nw-resize", "sw-resize", + { fromComp: "url(\"" + _testPath + "/a.cur\"), " + + "url(\"" + _testPath + "/b.cur\"), " + + "nw-resize"}), + ]), + new TestcaseBundle(gPropList.direction, [ + new AnimTestcaseFromTo("ltr", "rtl"), + new AnimTestcaseFromTo("rtl", "inherit"), + ]), + new TestcaseBundle(gPropList.display, [ + // I'm not testing the "inherit" value for "display", because part of + // my test runs with "display: none" on everything, and so the + // inherited value isn't always the same. (i.e. the computed value + // of 'inherit' will be different in different tests) + new AnimTestcaseFromTo("block", "table-cell"), + new AnimTestcaseFromTo("inline", "inline-table"), + new AnimTestcaseFromTo("table-row", "none"), + ]), + new TestcaseBundle(gPropList.dominant_baseline, [ + new AnimTestcaseFromTo("use-script", "no-change"), + new AnimTestcaseFromTo("reset-size", "ideographic"), + new AnimTestcaseFromTo("alphabetic", "hanging"), + new AnimTestcaseFromTo("mathematical", "central"), + new AnimTestcaseFromTo("middle", "text-after-edge"), + new AnimTestcaseFromTo("text-before-edge", "auto"), + new AnimTestcaseFromTo("use-script", "inherit", { toComp: "auto" } ), + ]), + // NOTE: Mozilla doesn't currently support "enable-background", but I'm + // testing it here in case we ever add support for it, because it's + // explicitly not animatable in the SVG spec. + new TestcaseBundle(gPropList.enable_background, [ + new AnimTestcaseFromTo("new", "accumulate"), + ]), + new TestcaseBundle(gPropList.fill, + [].concat(_fromToTestLists.color, + _fromToTestLists.paintServer, + _fromToTestLists.colorFromInheritBlack)), + new TestcaseBundle(gPropList.fill_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.fill_rule, [ + new AnimTestcaseFromTo("nonzero", "evenodd"), + new AnimTestcaseFromTo("evenodd", "inherit", { toComp: "nonzero" }), + ]), + new TestcaseBundle(gPropList.filter, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.flood_color, + [].concat(_fromToTestLists.color, + _fromToTestLists.colorFromInheritBlack)), + new TestcaseBundle(gPropList.flood_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.font, [ + // NOTE: 'line-height' is hard-wired at 10px in test_smilCSSFromTo.xhtml + // because if it's not explicitly set, its value varies across platforms. + // NOTE: System font values can't be tested here, because their computed + // values vary from platform to platform. However, they are tested + // visually, in the reftest "anim-css-font-1.svg" + new AnimTestcaseFromTo("10px serif", "30px serif", + { fromComp: "normal normal 400 10px / 10px serif", + toComp: "normal normal 400 30px / 10px serif"}), + new AnimTestcaseFromTo("10px serif", "30px sans-serif", + { fromComp: "normal normal 400 10px / 10px serif", + toComp: "normal normal 400 30px / 10px sans-serif"}), + new AnimTestcaseFromTo("1px / 90px cursive", "100px monospace", + { fromComp: "normal normal 400 1px / 10px cursive", + toComp: "normal normal 400 100px / 10px monospace"}), + new AnimTestcaseFromTo("italic small-caps 200 1px cursive", + "100px monospace", + { fromComp: "italic small-caps 200 1px / 10px cursive", + toComp: "normal normal 400 100px / 10px monospace"}), + new AnimTestcaseFromTo("oblique normal 200 30px / 10px cursive", + "normal small-caps 800 40px / 10px serif"), + ]), + new TestcaseBundle(gPropList.font_family, [ + new AnimTestcaseFromTo("serif", "sans-serif"), + new AnimTestcaseFromTo("cursive", "monospace"), + ]), + new TestcaseBundle(gPropList.font_size, + [].concat(_fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, [ + new AnimTestcaseFromTo("10px", "40%", { midComp: "15px", toComp: "20px" }), + new AnimTestcaseFromTo("160%", "80%", + { fromComp: "80px", + midComp: "60px", + toComp: "40px"}), + ])), + new TestcaseBundle(gPropList.font_size_adjust, [ + new AnimTestcaseFromTo("0.9", "0.1", { midComp: "0.5" }), + new AnimTestcaseFromTo("0.5", "0.6", { midComp: "0.55" }), + new AnimTestcaseFromTo("none", "0.4"), + ]), + new TestcaseBundle(gPropList.font_stretch, [ + new AnimTestcaseFromTo("normal", "wider", {}, + "need support for animating between " + + "relative 'font-stretch' values"), + new AnimTestcaseFromTo("narrower", "ultra-condensed", {}, + "need support for animating between " + + "relative 'font-stretch' values"), + new AnimTestcaseFromTo("ultra-condensed", "condensed", + { midComp: "extra-condensed" }), + new AnimTestcaseFromTo("semi-condensed", "semi-expanded", + { midComp: "normal" }), + new AnimTestcaseFromTo("expanded", "ultra-expanded", + { midComp: "extra-expanded" }), + new AnimTestcaseFromTo("ultra-expanded", "inherit", + { midComp: "expanded", toComp: "normal" }), + ]), + new TestcaseBundle(gPropList.font_style, [ + new AnimTestcaseFromTo("italic", "inherit", { toComp: "normal" }), + new AnimTestcaseFromTo("normal", "italic"), + new AnimTestcaseFromTo("italic", "oblique"), + new AnimTestcaseFromTo("oblique", "normal"), + ]), + new TestcaseBundle(gPropList.font_variant, [ + new AnimTestcaseFromTo("inherit", "small-caps", { fromComp: "normal" }), + new AnimTestcaseFromTo("small-caps", "normal"), + ]), + new TestcaseBundle(gPropList.font_weight, [ + new AnimTestcaseFromTo("100", "900", { midComp: "500" }), + new AnimTestcaseFromTo("700", "100", { midComp: "400" }), + new AnimTestcaseFromTo("inherit", "200", + { fromComp: "400", midComp: "300" }), + new AnimTestcaseFromTo("normal", "bold", + { fromComp: "400", midComp: "500", toComp: "700" }), + new AnimTestcaseFromTo("lighter", "bolder", {}, + "need support for animating between " + + "relative 'font-weight' values"), + ]), + // NOTE: Mozilla doesn't currently support "glyph-orientation-horizontal" or + // "glyph-orientation-vertical", but I'm testing them here in case we ever + // add support for them, because they're explicitly not animatable in the SVG + // spec. + new TestcaseBundle(gPropList.glyph_orientation_horizontal, + [ new AnimTestcaseFromTo("45deg", "60deg") ]), + new TestcaseBundle(gPropList.glyph_orientation_vertical, + [ new AnimTestcaseFromTo("45deg", "60deg") ]), + new TestcaseBundle(gPropList.image_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeQuality", + { toComp: "optimizequality" }), + new AnimTestcaseFromTo("optimizeQuality", "optimizeSpeed", + { fromComp: "optimizequality", + toComp: "optimizespeed" }), + ]), + new TestcaseBundle(gPropList.letter_spacing, + [].concat(_fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG)), + new TestcaseBundle(gPropList.letter_spacing, + _fromToTestLists.lengthPctSVG, + "pct->pct animations don't currently work for " + + "*-spacing properties"), + new TestcaseBundle(gPropList.lighting_color, + [].concat(_fromToTestLists.color, + _fromToTestLists.colorFromInheritWhite)), + new TestcaseBundle(gPropList.marker, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_end, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_mid, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_start, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.mask, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.overflow, [ + new AnimTestcaseFromTo("auto", "visible"), + new AnimTestcaseFromTo("inherit", "visible", { fromComp: "hidden" }), + new AnimTestcaseFromTo("scroll", "auto"), + ]), + new TestcaseBundle(gPropList.pointer_events, [ + new AnimTestcaseFromTo("visibleFill", "stroke", + { fromComp: "visiblefill" }), + new AnimTestcaseFromTo("none", "visibleStroke", + { toComp: "visiblestroke" }), + ]), + new TestcaseBundle(gPropList.shape_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeSpeed", + { toComp: "optimizespeed" }), + new AnimTestcaseFromTo("crispEdges", "geometricPrecision", + { fromComp: "crispedges", + toComp: "geometricprecision" }), + ]), + new TestcaseBundle(gPropList.stop_color, + [].concat(_fromToTestLists.color, + _fromToTestLists.colorFromInheritBlack)), + new TestcaseBundle(gPropList.stop_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.stroke, + [].concat(_fromToTestLists.color, + _fromToTestLists.paintServer, [ + // Note: inherited value is "none" (the default for "stroke" property) + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", + { fromComp: "none"})])), + new TestcaseBundle(gPropList.stroke_dasharray, + [].concat(_fromToTestLists.lengthPctSVG, + [ + new AnimTestcaseFromTo("inherit", "20", { fromComp: "none"}), + new AnimTestcaseFromTo("1", "none"), + new AnimTestcaseFromTo("10", "20", { midComp: "15"}), + new AnimTestcaseFromTo("1", "2, 3", { fromComp: "1, 1", + midComp: "1.5, 2"}), + new AnimTestcaseFromTo("2, 8", "6", { midComp: "4, 7"}), + new AnimTestcaseFromTo("1, 3", "1, 3, 5, 7, 9", + { fromComp: "1, 3, 1, 3, 1, 3, 1, 3, 1, 3", + midComp: "1, 3, 3, 5, 5, 2, 2, 4, 4, 6"}), + ])), + new TestcaseBundle(gPropList.stroke_dashoffset, + [].concat(_fromToTestLists.lengthNoUnitsSVG, + _fromToTestLists.lengthPxSVG, + _fromToTestLists.lengthPxPctSVG, + _fromToTestLists.lengthPctSVG, + _fromToTestLists.lengthPxNoUnitsSVG)), + new TestcaseBundle(gPropList.stroke_linecap, [ + new AnimTestcaseFromTo("butt", "round"), + new AnimTestcaseFromTo("round", "square"), + ]), + new TestcaseBundle(gPropList.stroke_linejoin, [ + new AnimTestcaseFromTo("miter", "round"), + new AnimTestcaseFromTo("round", "bevel"), + ]), + new TestcaseBundle(gPropList.stroke_miterlimit, [ + new AnimTestcaseFromTo("1", "2", { midComp: "1.5" }), + new AnimTestcaseFromTo("20.1", "10.1", { midComp: "15.1" }), + ]), + new TestcaseBundle(gPropList.stroke_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.stroke_width, + [].concat(_fromToTestLists.lengthNoUnitsSVG, + _fromToTestLists.lengthPxSVG, + _fromToTestLists.lengthPxPctSVG, + _fromToTestLists.lengthPctSVG, + _fromToTestLists.lengthPxNoUnitsSVG, [ + new AnimTestcaseFromTo("inherit", "7px", + { fromComp: "1", midComp: "4", toComp: "7" }), + ])), + new TestcaseBundle(gPropList.text_anchor, [ + new AnimTestcaseFromTo("start", "middle"), + new AnimTestcaseFromTo("middle", "end"), + ]), + new TestcaseBundle(gPropList.text_decoration, [ + new AnimTestcaseFromTo("none", "underline"), + new AnimTestcaseFromTo("overline", "line-through"), + new AnimTestcaseFromTo("blink", "underline"), + ]), + new TestcaseBundle(gPropList.text_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeSpeed", + { toComp: "optimizespeed" }), + new AnimTestcaseFromTo("optimizeSpeed", "geometricPrecision", + { fromComp: "optimizespeed", + toComp: "geometricprecision" }), + new AnimTestcaseFromTo("geometricPrecision", "optimizeLegibility", + { fromComp: "geometricprecision", + toComp: "optimizelegibility" }), + ]), + new TestcaseBundle(gPropList.unicode_bidi, [ + new AnimTestcaseFromTo("embed", "bidi-override"), + ]), + new TestcaseBundle(gPropList.vector_effect, [ + new AnimTestcaseFromTo("none", "non-scaling-stroke"), + ]), + new TestcaseBundle(gPropList.visibility, [ + new AnimTestcaseFromTo("visible", "hidden"), + new AnimTestcaseFromTo("hidden", "collapse"), + ]), + new TestcaseBundle(gPropList.word_spacing, + [].concat(_fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG)), + new TestcaseBundle(gPropList.word_spacing, + _fromToTestLists.lengthPctSVG, + "pct->pct animations don't currently work for " + + "*-spacing properties"), + // NOTE: Mozilla doesn't currently support "writing-mode", but I'm + // testing it here in case we ever add support for it, because it's + // explicitly not animatable in the SVG spec. + new TestcaseBundle(gPropList.writing_mode, [ + new AnimTestcaseFromTo("lr", "rl"), + ]), +]; diff --git a/dom/smil/test/db_smilCSSPaced.js b/dom/smil/test/db_smilCSSPaced.js new file mode 100644 index 000000000..3f069691f --- /dev/null +++ b/dom/smil/test/db_smilCSSPaced.js @@ -0,0 +1,321 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* vim: set shiftwidth=4 tabstop=4 autoindent cindent noexpandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* testcase data for paced-mode animations of CSS properties */ + +// Lists of testcases for re-use across multiple properties of the same type +var _pacedTestLists = +{ + color: [ + new AnimTestcasePaced("rgb(2, 4, 6); " + + "rgb(4, 8, 12); " + + "rgb(8, 16, 24)", + { comp0: "rgb(2, 4, 6)", + comp1_6: "rgb(3, 6, 9)", + comp1_3: "rgb(4, 8, 12)", + comp2_3: "rgb(6, 12, 18)", + comp1: "rgb(8, 16, 24)" + }), + new AnimTestcasePaced("rgb(10, 10, 10); " + + "rgb(20, 10, 8); " + + "rgb(20, 30, 4)", + { comp0: "rgb(10, 10, 10)", + comp1_6: "rgb(15, 10, 9)", + comp1_3: "rgb(20, 10, 8)", + comp2_3: "rgb(20, 20, 6)", + comp1: "rgb(20, 30, 4)" + }), + new AnimTestcasePaced("olive; " + // rgb(128, 128, 0) + "currentColor; " + // rgb(50, 50, 50) + "rgb(206, 150, 206)", + { comp0: "rgb(128, 128, 0)", + comp1_6: "rgb(89, 89, 25)", + comp1_3: "rgb(50, 50, 50)", + comp2_3: "rgb(128, 100, 128)", + comp1: "rgb(206, 150, 206)" + }), + // Use the same RGB component values to make + // premultication effect easier to compute. + new AnimTestcasePaced("rgba(20, 40, 60, 0.2); " + + "rgba(20, 40, 60, 0.4); " + + "rgba(20, 40, 60, 0.8)", + { comp0: "rgba(20, 40, 60, 0.2)", + comp1_6: "rgba(20, 40, 60, 0.3)", + comp1_3: "rgba(20, 40, 60, 0.4)", + comp2_3: "rgba(20, 40, 60, 0.6)", + comp1: "rgba(20, 40, 60, 0.8)" + }), + ], + paintServer : [ + // Sanity check: These aren't interpolatable -- they should end up + // ignoring the calcMode="paced" and falling into discrete-mode. + new AnimTestcasePaced("url(#gradA); url(#gradB)", + { + comp0: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp1_6: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp1_3: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp2_3: "url(\"" + document.URL + "#gradB\") rgb(0, 0, 0)", + comp1: "url(\"" + document.URL + "#gradB\") rgb(0, 0, 0)" + }, + "need support for URI-based paints"), + new AnimTestcasePaced("url(#gradA); url(#gradB); url(#gradC)", + { + comp0: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp1_6: "url(\"" + document.URL + "#gradA\") rgb(0, 0, 0)", + comp1_3: "url(\"" + document.URL + "#gradB\") rgb(0, 0, 0)", + comp2_3: "url(\"" + document.URL + "#gradC\") rgb(0, 0, 0)", + comp1: "url(\"" + document.URL + "#gradC\") rgb(0, 0, 0)" + }, + "need support for URI-based paints"), + ], + lengthNoUnits : [ + new AnimTestcasePaced("2; 0; 4", + { comp0: "2px", + comp1_6: "1px", + comp1_3: "0px", + comp2_3: "2px", + comp1: "4px" + }), + new AnimTestcasePaced("10; 12; 8", + { comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px" + }), + ], + lengthNoUnitsSVG : [ + new AnimTestcasePaced("2; 0; 4", + { comp0: "2", + comp1_6: "1", + comp1_3: "0", + comp2_3: "2", + comp1: "4" + }), + new AnimTestcasePaced("10; 12; 8", + { comp0: "10", + comp1_6: "11", + comp1_3: "12", + comp2_3: "10", + comp1: "8" + }), + ], + lengthPx : [ + new AnimTestcasePaced("0px; 2px; 6px", + { comp0: "0px", + comp1_6: "1px", + comp1_3: "2px", + comp2_3: "4px", + comp1: "6px" + }), + new AnimTestcasePaced("10px; 12px; 8px", + { comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px" + }), + ], + lengthPxSVG : [ + new AnimTestcasePaced("0px; 2px; 6px", + { comp0: "0", + comp1_6: "1", + comp1_3: "2", + comp2_3: "4", + comp1: "6" + }), + new AnimTestcasePaced("10px; 12px; 8px", + { comp0: "10", + comp1_6: "11", + comp1_3: "12", + comp2_3: "10", + comp1: "8" + }), + ], + lengthPctSVG : [ + new AnimTestcasePaced("5%; 6%; 4%", + { comp0: "5%", + comp1_6: "5.5%", + comp1_3: "6%", + comp2_3: "5%", + comp1: "4%" + }), + ], + lengthPxPctSVG : [ + new AnimTestcasePaced("0px; 1%; 6px", + { comp0: "0px", + comp1_6: "1px", + comp1_3: "1%", + comp2_3: "4px", + comp1: "6px" + }, + "need support for interpolating between " + + "px and percent values"), + ], + opacity : [ + new AnimTestcasePaced("0; 0.2; 0.6", + { comp0: "0", + comp1_6: "0.1", + comp1_3: "0.2", + comp2_3: "0.4", + comp1: "0.6" + }), + new AnimTestcasePaced("0.7; 1.0; 0.4", + { comp0: "0.7", + comp1_6: "0.85", + comp1_3: "1", + comp2_3: "0.7", + comp1: "0.4" + }), + ], + rect : [ + new AnimTestcasePaced("rect(2px, 4px, 6px, 8px); " + + "rect(4px, 8px, 12px, 16px); " + + "rect(8px, 16px, 24px, 32px)", + { comp0: "rect(2px, 4px, 6px, 8px)", + comp1_6: "rect(3px, 6px, 9px, 12px)", + comp1_3: "rect(4px, 8px, 12px, 16px)", + comp2_3: "rect(6px, 12px, 18px, 24px)", + comp1: "rect(8px, 16px, 24px, 32px)" + }), + new AnimTestcasePaced("rect(10px, 10px, 10px, 10px); " + + "rect(20px, 10px, 50px, 8px); " + + "rect(20px, 30px, 130px, 4px)", + { comp0: "rect(10px, 10px, 10px, 10px)", + comp1_6: "rect(15px, 10px, 30px, 9px)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "rect(20px, 20px, 90px, 6px)", + comp1: "rect(20px, 30px, 130px, 4px)" + }), + new AnimTestcasePaced("rect(10px, auto, 10px, 10px); " + + "rect(20px, auto, 50px, 8px); " + + "rect(40px, auto, 130px, 4px)", + { comp0: "rect(10px, auto, 10px, 10px)", + comp1_6: "rect(15px, auto, 30px, 9px)", + comp1_3: "rect(20px, auto, 50px, 8px)", + comp2_3: "rect(30px, auto, 90px, 6px)", + comp1: "rect(40px, auto, 130px, 4px)" + }), + // Paced-mode animation is not supported in these next few cases + // (Can't compute subcomponent distance between 'auto' & px-values) + new AnimTestcasePaced("rect(10px, 10px, 10px, auto); " + + "rect(20px, 10px, 50px, 8px); " + + "rect(20px, 30px, 130px, 4px)", + { comp0: "rect(10px, 10px, 10px, auto)", + comp1_6: "rect(10px, 10px, 10px, auto)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "rect(20px, 30px, 130px, 4px)", + comp1: "rect(20px, 30px, 130px, 4px)" + }), + new AnimTestcasePaced("rect(10px, 10px, 10px, 10px); " + + "rect(20px, 10px, 50px, 8px); " + + "auto", + { comp0: "rect(10px, 10px, 10px, 10px)", + comp1_6: "rect(10px, 10px, 10px, 10px)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "auto", + comp1: "auto" + }), + new AnimTestcasePaced("auto; " + + "auto; " + + "rect(20px, 30px, 130px, 4px)", + { comp0: "auto", + comp1_6: "auto", + comp1_3: "auto", + comp2_3: "rect(20px, 30px, 130px, 4px)", + comp1: "rect(20px, 30px, 130px, 4px)" + }), + new AnimTestcasePaced("auto; auto; auto", + { comp0: "auto", + comp1_6: "auto", + comp1_3: "auto", + comp2_3: "auto", + comp1: "auto" + }), + ], +}; + +// TODO: test more properties here. +var gPacedBundles = +[ + new TestcaseBundle(gPropList.clip, _pacedTestLists.rect), + new TestcaseBundle(gPropList.color, _pacedTestLists.color), + new TestcaseBundle(gPropList.direction, [ + new AnimTestcasePaced("rtl; ltr; rtl") + ]), + new TestcaseBundle(gPropList.fill, + [].concat(_pacedTestLists.color, + _pacedTestLists.paintServer)), + new TestcaseBundle(gPropList.font_size, + [].concat(_pacedTestLists.lengthNoUnits, + _pacedTestLists.lengthPx, [ + new AnimTestcasePaced("20%; 24%; 16%", + { comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px" + }), + new AnimTestcasePaced("0px; 4%; 6px", + { comp0: "0px", + comp1_6: "1px", + comp1_3: "2px", + comp2_3: "4px", + comp1: "6px" + }), + ]) + ), + new TestcaseBundle(gPropList.font_size_adjust, [ + new AnimTestcasePaced("0.2; 0.6; 0.8", + { comp0: "0.2", + comp1_6: "0.3", + comp1_3: "0.4", + comp2_3: "0.6", + comp1: "0.8" + }), + new AnimTestcasePaced("none; none; 0.5", + { comp0: "none", + comp1_6: "none", + comp1_3: "none", + comp2_3: "0.5", + comp1: "0.5" + }), + ]), + new TestcaseBundle(gPropList.font_family, [ + // Sanity check: 'font-family' isn't interpolatable. It should end up + // ignoring the calcMode="paced" and falling into discrete-mode. + new AnimTestcasePaced("serif; sans-serif; monospace", + { comp0: "serif", + comp1_6: "serif", + comp1_3: "sans-serif", + comp2_3: "monospace", + comp1: "monospace" + }, + "need support for more font properties"), + ]), + new TestcaseBundle(gPropList.opacity, _pacedTestLists.opacity), + new TestcaseBundle(gPropList.stroke_dasharray, + [].concat(_pacedTestLists.lengthPctSVG, [ + new AnimTestcasePaced("7, 7, 7; 7, 10, 3; 1, 2, 3", + { comp0: "7, 7, 7", + comp1_6: "7, 8.5, 5", + comp1_3: "7, 10, 3", + comp2_3: "4, 6, 3", + comp1: "1, 2, 3" + }), + ])), + new TestcaseBundle(gPropList.stroke_dashoffset, + [].concat(_pacedTestLists.lengthNoUnitsSVG, + _pacedTestLists.lengthPxSVG, + _pacedTestLists.lengthPctSVG, + _pacedTestLists.lengthPxPctSVG)), + new TestcaseBundle(gPropList.stroke_width, + [].concat(_pacedTestLists.lengthNoUnitsSVG, + _pacedTestLists.lengthPxSVG, + _pacedTestLists.lengthPctSVG, + _pacedTestLists.lengthPxPctSVG)), + // XXXdholbert TODO: test 'stroke-dasharray' once we support animating it +]; diff --git a/dom/smil/test/db_smilCSSPropertyList.js b/dom/smil/test/db_smilCSSPropertyList.js new file mode 100644 index 000000000..f9f3de62e --- /dev/null +++ b/dom/smil/test/db_smilCSSPropertyList.js @@ -0,0 +1,93 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* list of CSS properties recognized by SVG 1.1 spec, for use in mochitests */ + +// List of CSS Properties from SVG 1.1 Specification, Appendix N +var gPropList = +{ + // NOTE: AnimatedAttribute signature is: + // (attrName, attrType, sampleTarget, isAnimatable, isAdditive) + + // SKIP 'alignment-baseline' property: animatable but not supported by Mozilla + // SKIP 'baseline-shift' property: animatable but not supported by Mozilla + clip: new AdditiveAttribute("clip", "CSS", "marker"), + clip_path: new NonAdditiveAttribute("clip-path", "CSS", "rect"), + clip_rule: new NonAdditiveAttribute("clip-rule", "CSS", "circle"), + color: new AdditiveAttribute("color", "CSS", "rect"), + color_interpolation: + new NonAdditiveAttribute("color-interpolation", "CSS", "rect"), + color_interpolation_filters: + new NonAdditiveAttribute("color-interpolation-filters", "CSS", + "feFlood"), + // SKIP 'color-profile' property: animatable but not supported by Mozilla + // SKIP 'color-rendering' property: animatable but not supported by Mozilla + cursor: new NonAdditiveAttribute("cursor", "CSS", "rect"), + direction: new NonAnimatableAttribute("direction", "CSS", "text"), + display: new NonAdditiveAttribute("display", "CSS", "rect"), + dominant_baseline: + new NonAdditiveAttribute("dominant-baseline", "CSS", "text"), + enable_background: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + new NonAnimatableAttribute("enable-background", "CSS", "marker"), + fill: new AdditiveAttribute("fill", "CSS", "rect"), + fill_opacity: new AdditiveAttribute("fill-opacity", "CSS", "rect"), + fill_rule: new NonAdditiveAttribute("fill-rule", "CSS", "rect"), + filter: new NonAdditiveAttribute("filter", "CSS", "rect"), + flood_color: new AdditiveAttribute("flood-color", "CSS", "feFlood"), + flood_opacity: new AdditiveAttribute("flood-opacity", "CSS", "feFlood"), + font: new NonAdditiveAttribute("font", "CSS", "text"), + font_family: new NonAdditiveAttribute("font-family", "CSS", "text"), + font_size: new AdditiveAttribute("font-size", "CSS", "text"), + font_size_adjust: + new NonAdditiveAttribute("font-size-adjust", "CSS", "text"), + font_stretch: new NonAdditiveAttribute("font-stretch", "CSS", "text"), + font_style: new NonAdditiveAttribute("font-style", "CSS", "text"), + font_variant: new NonAdditiveAttribute("font-variant", "CSS", "text"), + // XXXdholbert should 'font-weight' be additive? + font_weight: new NonAdditiveAttribute("font-weight", "CSS", "text"), + glyph_orientation_horizontal: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "CSS", "text"), + glyph_orientation_vertical: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "CSS", "text"), + image_rendering: + NonAdditiveAttribute("image-rendering", "CSS", "image"), + // SKIP 'kerning' property: animatable but not supported by Mozilla + letter_spacing: new AdditiveAttribute("letter-spacing", "CSS", "text"), + lighting_color: + new AdditiveAttribute("lighting-color", "CSS", "feDiffuseLighting"), + marker: new NonAdditiveAttribute("marker", "CSS", "line"), + marker_end: new NonAdditiveAttribute("marker-end", "CSS", "line"), + marker_mid: new NonAdditiveAttribute("marker-mid", "CSS", "line"), + marker_start: new NonAdditiveAttribute("marker-start", "CSS", "line"), + mask: new NonAdditiveAttribute("mask", "CSS", "line"), + opacity: new AdditiveAttribute("opacity", "CSS", "rect"), + overflow: new NonAdditiveAttribute("overflow", "CSS", "marker"), + pointer_events: new NonAdditiveAttribute("pointer-events", "CSS", "rect"), + shape_rendering: new NonAdditiveAttribute("shape-rendering", "CSS", "rect"), + stop_color: new AdditiveAttribute("stop-color", "CSS", "stop"), + stop_opacity: new AdditiveAttribute("stop-opacity", "CSS", "stop"), + stroke: new AdditiveAttribute("stroke", "CSS", "rect"), + stroke_dasharray: new NonAdditiveAttribute("stroke-dasharray", "CSS", "rect"), + stroke_dashoffset: new AdditiveAttribute("stroke-dashoffset", "CSS", "rect"), + stroke_linecap: new NonAdditiveAttribute("stroke-linecap", "CSS", "rect"), + stroke_linejoin: new NonAdditiveAttribute("stroke-linejoin", "CSS", "rect"), + stroke_miterlimit: new AdditiveAttribute("stroke-miterlimit", "CSS", "rect"), + stroke_opacity: new AdditiveAttribute("stroke-opacity", "CSS", "rect"), + stroke_width: new AdditiveAttribute("stroke-width", "CSS", "rect"), + text_anchor: new NonAdditiveAttribute("text-anchor", "CSS", "text"), + text_decoration: new NonAdditiveAttribute("text-decoration", "CSS", "text"), + text_rendering: new NonAdditiveAttribute("text-rendering", "CSS", "text"), + unicode_bidi: new NonAnimatableAttribute("unicode-bidi", "CSS", "text"), + vector_effect: new NonAdditiveAttribute("vector-effect", "CSS", "rect"), + visibility: new NonAdditiveAttribute("visibility", "CSS", "rect"), + word_spacing: new AdditiveAttribute("word-spacing", "CSS", "text"), + writing_mode: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + new NonAnimatableAttribute("writing-mode", "CSS", "text"), +}; diff --git a/dom/smil/test/db_smilMappedAttrList.js b/dom/smil/test/db_smilMappedAttrList.js new file mode 100644 index 000000000..ede5dc23b --- /dev/null +++ b/dom/smil/test/db_smilMappedAttrList.js @@ -0,0 +1,131 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* List of SVG presentational attributes in the SVG 1.1 spec, for use in + mochitests. (These are the attributes that are mapped to CSS properties) */ + +var gMappedAttrList = +{ + // NOTE: The list here should match the MappedAttributeEntry arrays in + // nsSVGElement.cpp + + // PresentationAttributes-FillStroke + fill: new AdditiveAttribute("fill", "XML", "rect"), + fill_opacity: new AdditiveAttribute("fill-opacity", "XML", "rect"), + fill_rule: new NonAdditiveAttribute("fill-rule", "XML", "rect"), + stroke: new AdditiveAttribute("stroke", "XML", "rect"), + stroke_dasharray: + new NonAdditiveAttribute("stroke-dasharray", "XML", "rect"), + stroke_dashoffset: new AdditiveAttribute("stroke-dashoffset", "XML", "rect"), + stroke_linecap: new NonAdditiveAttribute("stroke-linecap", "XML", "rect"), + stroke_linejoin: new NonAdditiveAttribute("stroke-linejoin", "XML", "rect"), + stroke_miterlimit: new AdditiveAttribute("stroke-miterlimit", "XML", "rect"), + stroke_opacity: new AdditiveAttribute("stroke-opacity", "XML", "rect"), + stroke_width: new AdditiveAttribute("stroke-width", "XML", "rect"), + + // PresentationAttributes-Graphics + clip_path: new NonAdditiveAttribute("clip-path", "XML", "rect"), + clip_rule: new NonAdditiveAttribute("clip-rule", "XML", "circle"), + color_interpolation: + new NonAdditiveAttribute("color-interpolation", "XML", "rect"), + cursor: new NonAdditiveAttribute("cursor", "XML", "rect"), + display: new NonAdditiveAttribute("display", "XML", "rect"), + filter: new NonAdditiveAttribute("filter", "XML", "rect"), + image_rendering: + NonAdditiveAttribute("image-rendering", "XML", "image"), + mask: new NonAdditiveAttribute("mask", "XML", "line"), + pointer_events: new NonAdditiveAttribute("pointer-events", "XML", "rect"), + shape_rendering: new NonAdditiveAttribute("shape-rendering", "XML", "rect"), + text_rendering: new NonAdditiveAttribute("text-rendering", "XML", "text"), + visibility: new NonAdditiveAttribute("visibility", "XML", "rect"), + + // PresentationAttributes-TextContentElements + // SKIP 'alignment-baseline' property: animatable but not supported by Mozilla + // SKIP 'baseline-shift' property: animatable but not supported by Mozilla + direction: new NonAnimatableAttribute("direction", "XML", "text"), + dominant_baseline: + new NonAdditiveAttribute("dominant-baseline", "XML", "text"), + glyph_orientation_horizontal: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "XML", "text"), + glyph_orientation_vertical: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "XML", "text"), + // SKIP 'kerning' property: animatable but not supported by Mozilla + letter_spacing: new AdditiveAttribute("letter-spacing", "XML", "text"), + text_anchor: new NonAdditiveAttribute("text-anchor", "XML", "text"), + text_decoration: new NonAdditiveAttribute("text-decoration", "XML", "text"), + unicode_bidi: new NonAnimatableAttribute("unicode-bidi", "XML", "text"), + word_spacing: new AdditiveAttribute("word-spacing", "XML", "text"), + + // PresentationAttributes-FontSpecification + font_family: new NonAdditiveAttribute("font-family", "XML", "text"), + font_size: new AdditiveAttribute("font-size", "XML", "text"), + font_size_adjust: + new NonAdditiveAttribute("font-size-adjust", "XML", "text"), + font_stretch: new NonAdditiveAttribute("font-stretch", "XML", "text"), + font_style: new NonAdditiveAttribute("font-style", "XML", "text"), + font_variant: new NonAdditiveAttribute("font-variant", "XML", "text"), + font_weight: new NonAdditiveAttribute("font-weight", "XML", "text"), + + // PresentationAttributes-GradientStop + stop_color: new AdditiveAttribute("stop-color", "XML", "stop"), + stop_opacity: new AdditiveAttribute("stop-opacity", "XML", "stop"), + + // PresentationAttributes-Viewports + overflow: new NonAdditiveAttribute("overflow", "XML", "marker"), + clip: new AdditiveAttribute("clip", "XML", "marker"), + + // PresentationAttributes-Makers + marker_end: new NonAdditiveAttribute("marker-end", "XML", "line"), + marker_mid: new NonAdditiveAttribute("marker-mid", "XML", "line"), + marker_start: new NonAdditiveAttribute("marker-start", "XML", "line"), + + // PresentationAttributes-Color + color: new AdditiveAttribute("color", "XML", "rect"), + + // PresentationAttributes-Filters + color_interpolation_filters: + new NonAdditiveAttribute("color-interpolation-filters", "XML", + "feFlood"), + + // PresentationAttributes-feFlood + flood_color: new AdditiveAttribute("flood-color", "XML", "feFlood"), + flood_opacity: new AdditiveAttribute("flood-opacity", "XML", "feFlood"), + + // PresentationAttributes-LightingEffects + lighting_color: + new AdditiveAttribute("lighting-color", "XML", "feDiffuseLighting"), +}; + +// Utility method to copy a list of TestcaseBundle objects for CSS properties +// into a list of TestcaseBundles for the corresponding mapped attributes. +function convertCSSBundlesToMappedAttr(bundleList) { + // Create mapping of property names to the corresponding + // mapped-attribute object in gMappedAttrList. + var propertyNameToMappedAttr = {}; + for (attributeLabel in gMappedAttrList) { + var propName = gMappedAttrList[attributeLabel].attrName; + propertyNameToMappedAttr[propName] = gMappedAttrList[attributeLabel]; + } + + var convertedBundles = []; + for (var bundleIdx in bundleList) { + var origBundle = bundleList[bundleIdx]; + var propName = origBundle.animatedAttribute.attrName; + if (propertyNameToMappedAttr[propName]) { + // There's a mapped attribute by this name! Duplicate the TestcaseBundle, + // using the Mapped Attribute instead of the CSS Property. + is(origBundle.animatedAttribute.attrType, "CSS", + "expecting to be converting from CSS to XML"); + convertedBundles.push( + new TestcaseBundle(propertyNameToMappedAttr[propName], + origBundle.testcaseList, + origBundle.skipReason)); + } + } + return convertedBundles; +} diff --git a/dom/smil/test/mochitest.ini b/dom/smil/test/mochitest.ini new file mode 100644 index 000000000..b5a0c51bb --- /dev/null +++ b/dom/smil/test/mochitest.ini @@ -0,0 +1,60 @@ +[DEFAULT] +support-files = + db_smilAnimateMotion.js + db_smilCSSFromBy.js + db_smilCSSFromTo.js + db_smilCSSPaced.js + db_smilCSSPropertyList.js + db_smilMappedAttrList.js + smilAnimateMotionValueLists.js + smilExtDoc_helper.svg + smilTestUtils.js + smilXHR_helper.svg + +[test_smilAccessKey.xhtml] +[test_smilAnimateMotion.xhtml] +[test_smilAnimateMotionInvalidValues.xhtml] +[test_smilAnimateMotionOverrideRules.xhtml] +[test_smilBackwardsSeeking.xhtml] +[test_smilCSSFontStretchRelative.xhtml] +[test_smilCSSFromBy.xhtml] +[test_smilCSSFromTo.xhtml] +# [test_smilCSSInherit.xhtml] +# disabled until bug 501183 is fixed +[test_smilCSSInvalidValues.xhtml] +[test_smilCSSPaced.xhtml] +[test_smilChangeAfterFrozen.xhtml] +[test_smilConditionalProcessing.html] +[test_smilContainerBinding.xhtml] +[test_smilCrossContainer.xhtml] +[test_smilDynamicDelayedBeginElement.xhtml] +[test_smilExtDoc.xhtml] +skip-if = toolkit == 'android' +[test_smilFillMode.xhtml] +[test_smilGetSimpleDuration.xhtml] +[test_smilGetStartTime.xhtml] +[test_smilHyperlinking.xhtml] +[test_smilInvalidValues.html] +[test_smilKeySplines.xhtml] +[test_smilKeyTimes.xhtml] +[test_smilKeyTimesPacedMode.xhtml] +[test_smilMappedAttrFromBy.xhtml] +[test_smilMappedAttrFromTo.xhtml] +[test_smilMappedAttrPaced.xhtml] +[test_smilMinTiming.html] +[test_smilRepeatDuration.html] +[test_smilRepeatTiming.xhtml] +skip-if = toolkit == 'android' #TIMED_OUT +[test_smilReset.xhtml] +[test_smilRestart.xhtml] +[test_smilSetCurrentTime.xhtml] +[test_smilSync.xhtml] +[test_smilSyncTransform.xhtml] +[test_smilSyncbaseTarget.xhtml] +[test_smilTextZoom.xhtml] +[test_smilTimeEvents.xhtml] +[test_smilTiming.xhtml] +[test_smilTimingZeroIntervals.xhtml] +[test_smilUpdatedInterval.xhtml] +[test_smilValues.xhtml] +[test_smilXHR.xhtml] diff --git a/dom/smil/test/smilAnimateMotionValueLists.js b/dom/smil/test/smilAnimateMotionValueLists.js new file mode 100644 index 000000000..364bc250e --- /dev/null +++ b/dom/smil/test/smilAnimateMotionValueLists.js @@ -0,0 +1,128 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Lists of valid & invalid values for the various <animateMotion> attributes */ +const gValidValues = [ + "10 10", + "10 10;", // Trailing semicolons are allowed + "10 10; ", + " 10 10em ", + "1 2 ; 3,4", + "1,2;3,4", + "0 0", + "0,0", +]; + +const gInvalidValues = [ + ";10 10", + "10 10;;", + "1 2 3", + "1 2 3 4", + "1,2;3,4 ,", + ",", " , ", + ";", " ; ", + "a", " a; ", ";a;", + "", " ", + "1,2;3,4,", + "1,,2", + ",1,2", +]; + +const gValidRotate = [ + "10", + "20.1", + "30.5deg", + "0.5rad", + "auto", + "auto-reverse" +]; + +const gInvalidRotate = [ + " 10 ", + " 10deg", + "10 deg", + "10deg ", + "10 rad ", + "aaa", + " 10.1 ", +]; + +const gValidToBy = [ + "0 0", + "1em,2", + "50.3em 0.2in", + " 1,2", + "1 2 " +]; + +const gInvalidToBy = [ + "0 0 0", + "0 0,0", + "0,0,0", + "1emm 2", + "1 2;", + "1 2,", + " 1,2 ,", + "abc", + ",", + "", + "1,,2", + "1,2," +]; + +const gValidPath = [ + "m0 0 L30 30", + "M20,20L10 10", + "M20,20 L30, 30h20", + "m50 50", "M50 50", + "m0 0", "M0, 0" +]; + +// paths must start with at least a valid "M" segment to be valid +const gInvalidPath = [ + "M20in 20", + "h30", + "L50 50", + "abc", +]; + +// paths that at least start with a valid "M" segment are valid - the spec says +// to parse everything up to the first invalid token +const gValidPathWithErrors = [ + "M20 20em", + "m0 0 L30,,30", + "M10 10 L50 50 abc", +]; + +const gValidKeyPoints = [ + "0; 0.5; 1", + "0;.5;1", + "0; 0; 1", + "0; 1; 1", + "0; 0; 1;", // Trailing semicolons are allowed + "0; 0; 1; ", + "0; 0.000; 1", + "0; 0.000001; 1", +]; + +// Should have 3 values to be valid. +// Same as number of keyTimes values +const gInvalidKeyPoints = [ + "0; 1", + "0; 0.5; 0.75; 1", + "0; 1;", + "0", + "1", + "a", + "", + " ", + "0; -0.1; 1", + "0; 1.1; 1", + "0; 0.1; 1.1", + "-0.1; 0.1; 1", + "0; a; 1", + "0;;1", +]; diff --git a/dom/smil/test/smilExtDoc_helper.svg b/dom/smil/test/smilExtDoc_helper.svg new file mode 100644 index 000000000..fbd9d091a --- /dev/null +++ b/dom/smil/test/smilExtDoc_helper.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <filter id="filter"> + <feFlood flood-color="red"> + <set attributeName="flood-color" to="lime" begin="0.001"/> + </feFlood> + </filter> +</svg> diff --git a/dom/smil/test/smilTestUtils.js b/dom/smil/test/smilTestUtils.js new file mode 100644 index 000000000..2304d499b --- /dev/null +++ b/dom/smil/test/smilTestUtils.js @@ -0,0 +1,858 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note: Class syntax roughly based on: +// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Inheritance +const SVG_NS = "http://www.w3.org/2000/svg"; +const XLINK_NS = "http://www.w3.org/1999/xlink"; + +const MPATH_TARGET_ID = "smilTestUtilsTestingPath"; + +function extend(child, supertype) +{ + child.prototype.__proto__ = supertype.prototype; +} + +// General Utility Methods +var SMILUtil = +{ + // Returns the first matched <svg> node in the document + getSVGRoot : function() + { + return SMILUtil.getFirstElemWithTag("svg"); + }, + + // Returns the first element in the document with the matching tag + getFirstElemWithTag : function(aTargetTag) + { + var elemList = document.getElementsByTagName(aTargetTag); + return (elemList.length == 0 ? null : elemList[0]); + }, + + // Simple wrapper for getComputedStyle + getComputedStyleSimple: function(elem, prop) + { + return window.getComputedStyle(elem, null).getPropertyValue(prop); + }, + + getAttributeValue: function(elem, attr) + { + if (attr.attrName == SMILUtil.getMotionFakeAttributeName()) { + // Fake motion "attribute" -- "computed value" is the element's CTM + return elem.getCTM(); + } + if (attr.attrType == "CSS") { + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); + } + if (attr.attrType == "XML") { + // XXXdholbert This is appropriate for mapped attributes, but not + // for other attributes. + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); + } + }, + + // Smart wrapper for getComputedStyle, which will generate a "fake" computed + // style for recognized shorthand properties (font, font-variant, overflow, marker) + getComputedStyleWrapper : function(elem, propName) + { + // Special cases for shorthand properties (which aren't directly queriable + // via getComputedStyle) + var computedStyle; + if (propName == "font") { + var subProps = ["font-style", "font-variant-caps", "font-weight", + "font-size", "line-height", "font-family"]; + for (var i in subProps) { + var subPropStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + if (subPropStyle) { + if (subProps[i] == "line-height") { + // There needs to be a "/" before line-height + subPropStyle = "/ " + subPropStyle; + } + if (!computedStyle) { + computedStyle = subPropStyle; + } else { + computedStyle = computedStyle + " " + subPropStyle; + } + } + } + } else if (propName == "font-variant") { + // xxx - this isn't completely correct but it's sufficient for what's + // being tested here + computedStyle = SMILUtil.getComputedStyleSimple(elem, "font-variant-caps"); + } else if (propName == "marker") { + var subProps = ["marker-end", "marker-mid", "marker-start"]; + for (var i in subProps) { + if (!computedStyle) { + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + } else { + is(computedStyle, SMILUtil.getComputedStyleSimple(elem, subProps[i]), + "marker sub-properties should match each other " + + "(they shouldn't be individually set)"); + } + } + } else if (propName == "overflow") { + var subProps = ["overflow-x", "overflow-y"]; + for (var i in subProps) { + if (!computedStyle) { + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + } else { + is(computedStyle, SMILUtil.getComputedStyleSimple(elem, subProps[i]), + "overflow sub-properties should match each other " + + "(they shouldn't be individually set)"); + } + } + } else { + computedStyle = SMILUtil.getComputedStyleSimple(elem, propName); + } + return computedStyle; + }, + + getMotionFakeAttributeName : function() { + return "_motion"; + }, +}; + + +var CTMUtil = +{ + CTM_COMPONENTS_ALL : ["a", "b", "c", "d", "e", "f"], + CTM_COMPONENTS_ROTATE : ["a", "b", "c", "d" ], + + // Function to generate a CTM Matrix from a "summary" + // (a 3-tuple containing [tX, tY, theta]) + generateCTM : function(aCtmSummary) + { + if (!aCtmSummary || aCtmSummary.length != 3) { + ok(false, "Unexpected CTM summary tuple length: " + aCtmSummary.length); + } + var tX = aCtmSummary[0]; + var tY = aCtmSummary[1]; + var theta = aCtmSummary[2]; + var cosTheta = Math.cos(theta); + var sinTheta = Math.sin(theta); + var newCtm = { a : cosTheta, c: -sinTheta, e: tX, + b : sinTheta, d: cosTheta, f: tY }; + return newCtm; + }, + + /// Helper for isCtmEqual + isWithinDelta : function(aTestVal, aExpectedVal, aErrMsg, aIsTodo) { + var testFunc = aIsTodo ? todo : ok; + const delta = 0.00001; // allowing margin of error = 10^-5 + ok(aTestVal >= aExpectedVal - delta && + aTestVal <= aExpectedVal + delta, + aErrMsg + " | got: " + aTestVal + ", expected: " + aExpectedVal); + }, + + assertCTMEqual : function(aLeftCtm, aRightCtm, aComponentsToCheck, + aErrMsg, aIsTodo) { + var foundCTMDifference = false; + for (var j in aComponentsToCheck) { + var curComponent = aComponentsToCheck[j]; + if (!aIsTodo) { + CTMUtil.isWithinDelta(aLeftCtm[curComponent], aRightCtm[curComponent], + aErrMsg + " | component: " + curComponent, false); + } else if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { + foundCTMDifference = true; + } + } + + if (aIsTodo) { + todo(!foundCTMDifference, aErrMsg + " | (currently marked todo)"); + } + }, + + assertCTMNotEqual : function(aLeftCtm, aRightCtm, aComponentsToCheck, + aErrMsg, aIsTodo) { + // CTM should not match initial one + var foundCTMDifference = false; + for (var j in aComponentsToCheck) { + var curComponent = aComponentsToCheck[j]; + if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { + foundCTMDifference = true; + break; // We found a difference, as expected. Success! + } + } + + if (aIsTodo) { + todo(foundCTMDifference, aErrMsg + " | (currently marked todo)"); + } else { + ok(foundCTMDifference, aErrMsg); + } + }, +}; + + +// Wrapper for timing information +function SMILTimingData(aBegin, aDur) +{ + this._begin = aBegin; + this._dur = aDur; +} +SMILTimingData.prototype = +{ + _begin: null, + _dur: null, + getBeginTime : function() { return this._begin; }, + getDur : function() { return this._dur; }, + getEndTime : function() { return this._begin + this._dur; }, + getFractionalTime : function(aPortion) + { + return this._begin + aPortion * this._dur; + }, +} + +/** + * Attribute: a container for information about an attribute we'll + * attempt to animate with SMIL in our tests. + * + * See also the factory methods below: NonAnimatableAttribute(), + * NonAdditiveAttribute(), and AdditiveAttribute(). + * + * @param aAttrName The name of the attribute + * @param aAttrType The type of the attribute ("CSS" vs "XML") + * @param aTargetTag The name of an element that this attribute could be + * applied to. + * @param aIsAnimatable A bool indicating whether this attribute is defined as + * animatable in the SVG spec. + * @param aIsAdditive A bool indicating whether this attribute is defined as + * additive (i.e. supports "by" animation) in the SVG spec. + */ +function Attribute(aAttrName, aAttrType, aTargetTag, + aIsAnimatable, aIsAdditive) +{ + this.attrName = aAttrName; + this.attrType = aAttrType; + this.targetTag = aTargetTag; + this.isAnimatable = aIsAnimatable; + this.isAdditive = aIsAdditive; +} +Attribute.prototype = +{ + // Member variables + attrName : null, + attrType : null, + isAnimatable : null, + testcaseList : null, +}; + +// Generators for Attribute objects. These allow lists of attribute +// definitions to be more human-readible than if we were using Attribute() with +// boolean flags, e.g. "Attribute(..., true, true), Attribute(..., true, false) +function NonAnimatableAttribute(aAttrName, aAttrType, aTargetTag) +{ + return new Attribute(aAttrName, aAttrType, aTargetTag, false, false); +} +function NonAdditiveAttribute(aAttrName, aAttrType, aTargetTag) +{ + return new Attribute(aAttrName, aAttrType, aTargetTag, true, false); +} +function AdditiveAttribute(aAttrName, aAttrType, aTargetTag) +{ + return new Attribute(aAttrName, aAttrType, aTargetTag, true, true); +} + +/** + * TestcaseBundle: a container for a group of tests for a particular attribute + * + * @param aAttribute An Attribute object for the attribute + * @param aTestcaseList An array of AnimTestcase objects + */ +function TestcaseBundle(aAttribute, aTestcaseList, aSkipReason) +{ + this.animatedAttribute = aAttribute; + this.testcaseList = aTestcaseList; + this.skipReason = aSkipReason; +} +TestcaseBundle.prototype = +{ + // Member variables + animatedAttribute : null, + testcaseList : null, + skipReason : null, + + // Methods + go : function(aTimingData) { + if (this.skipReason) { + todo(false, "Skipping a bundle for '" + this.animatedAttribute.attrName + + "' because: " + this.skipReason); + } else { + // Sanity Check: Bundle should have > 0 testcases + if (!this.testcaseList || !this.testcaseList.length) { + ok(false, "a bundle for '" + this.animatedAttribute.attrName + + "' has no testcases"); + } + + var targetElem = + SMILUtil.getFirstElemWithTag(this.animatedAttribute.targetTag); + + if (!targetElem) { + ok(false, "Error: can't find an element of type '" + + this.animatedAttribute.targetTag + + "', so I can't test property '" + + this.animatedAttribute.attrName + "'"); + return; + } + + for (var testcaseIdx in this.testcaseList) { + var testcase = this.testcaseList[testcaseIdx]; + if (testcase.skipReason) { + todo(false, "Skipping a testcase for '" + + this.animatedAttribute.attrName + + "' because: " + testcase.skipReason); + } else { + testcase.runTest(targetElem, this.animatedAttribute, + aTimingData, false); + testcase.runTest(targetElem, this.animatedAttribute, + aTimingData, true); + } + } + } + }, +}; + +/** + * AnimTestcase: an abstract class that represents an animation testcase. + * (e.g. a set of "from"/"to" values to test) + */ +function AnimTestcase() {} // abstract => no constructor +AnimTestcase.prototype = +{ + // Member variables + _animElementTagName : "animate", // Can be overridden for e.g. animateColor + computedValMap : null, + skipReason : null, + + // Methods + /** + * runTest: Runs this AnimTestcase + * + * @param aTargetElem The node to be targeted in our test animation. + * @param aTargetAttr An Attribute object representing the attribute + * to be targeted in our test animation. + * @param aTimeData A SMILTimingData object with timing information for + * our test animation. + * @param aIsFreeze If true, indicates that our test animation should use + * fill="freeze"; otherwise, we'll default to fill="remove". + */ + runTest : function(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) + { + // SANITY CHECKS + if (!SMILUtil.getSVGRoot().animationsPaused()) { + ok(false, "Should start each test with animations paused"); + } + if (SMILUtil.getSVGRoot().getCurrentTime() != 0) { + ok(false, "Should start each test at time = 0"); + } + + // SET UP + // Cache initial computed value + var baseVal = SMILUtil.getAttributeValue(aTargetElem, aTargetAttr); + + // Create & append animation element + var anim = this.setupAnimationElement(aTargetAttr, aTimeData, aIsFreeze); + aTargetElem.appendChild(anim); + + // Build a list of [seek-time, expectedValue, errorMessage] triplets + var seekList = this.buildSeekList(aTargetAttr, baseVal, aTimeData, aIsFreeze); + + // DO THE ACTUAL TESTING + this.seekAndTest(seekList, aTargetElem, aTargetAttr); + + // CLEAN UP + aTargetElem.removeChild(anim); + SMILUtil.getSVGRoot().setCurrentTime(0); + }, + + // HELPER FUNCTIONS + // setupAnimationElement: <animate> element + // Subclasses should extend this parent method + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + var animElement = document.createElementNS(SVG_NS, + this._animElementTagName); + animElement.setAttribute("attributeName", aAnimAttr.attrName); + animElement.setAttribute("attributeType", aAnimAttr.attrType); + animElement.setAttribute("begin", aTimeData.getBeginTime()); + animElement.setAttribute("dur", aTimeData.getDur()); + if (aIsFreeze) { + animElement.setAttribute("fill", "freeze"); + } + return animElement; + }, + + buildSeekList : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + if (!aAnimAttr.isAnimatable) { + return this.buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, + "defined as non-animatable in SVG spec"); + } + if (this.computedValMap.noEffect) { + return this.buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, + "testcase specified to have no effect"); + } + return this.buildSeekListAnimated(aAnimAttr, aBaseVal, + aTimeData, aIsFreeze) + }, + + seekAndTest : function(aSeekList, aTargetElem, aTargetAttr) + { + var svg = document.getElementById("svg"); + for (var i in aSeekList) { + var entry = aSeekList[i]; + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); + is(SMILUtil.getAttributeValue(aTargetElem, aTargetAttr), + entry[1], entry[2]); + } + }, + + // methods that expect to be overridden in subclasses + buildSeekListStatic : function(aAnimAttr, aBaseVal, + aTimeData, aReasonStatic) {}, + buildSeekListAnimated : function(aAnimAttr, aBaseVal, + aTimeData, aIsFreeze) {}, +}; + + +// Abstract parent class to share code between from-to & from-by testcases. +function AnimTestcaseFrom() {} // abstract => no constructor +AnimTestcaseFrom.prototype = +{ + // Member variables + from : null, + + // Methods + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + // Call super, and then add my own customization + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, + [aAnimAttr, aTimeData, aIsFreeze]); + animElem.setAttribute("from", this.from) + return animElem; + }, + + buildSeekListStatic : function(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) + { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + + ": shouldn't be affected by animation "; + seekList.push([aTimeData.getBeginTime(), aBaseVal, + msgPrefix + "(at animation begin) - " + aReasonStatic]); + seekList.push([aTimeData.getFractionalTime(1/2), aBaseVal, + msgPrefix + "(at animation mid) - " + aReasonStatic]); + seekList.push([aTimeData.getEndTime(), aBaseVal, + msgPrefix + "(at animation end) - " + aReasonStatic]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), aBaseVal, + msgPrefix + "(after animation end) - " + aReasonStatic]); + return seekList; + }, + + buildSeekListAnimated : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + ": "; + if (aTimeData.getBeginTime() > 0.1) { + seekList.push([aTimeData.getBeginTime() - 0.1, + aBaseVal, + msgPrefix + "checking that base value is set " + + "before start of animation"]); + } + + seekList.push([aTimeData.getBeginTime(), + this.computedValMap.fromComp || this.from, + msgPrefix + "checking that 'from' value is set " + + "at start of animation"]); + seekList.push([aTimeData.getFractionalTime(1/2), + this.computedValMap.midComp || + this.computedValMap.toComp || this.to, + msgPrefix + "checking value halfway through animation"]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = this.computedValMap.toComp || this.to; + finalMsg = msgPrefix + "[freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = msgPrefix + + "[remove-mode] checking that animation is cleared "; + } + seekList.push([aTimeData.getEndTime(), + expectedEndVal, finalMsg + "at end of animation"]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, finalMsg + "after end of animation"]); + return seekList; + }, +} +extend(AnimTestcaseFrom, AnimTestcase); + +/* + * A testcase for a simple "from-to" animation + * @param aFrom The 'from' value + * @param aTo The 'to' value + * @param aComputedValMap A hash-map that contains some computed values, + * if they're needed, as follows: + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) + * - midComp: Computed value that we expect to visit halfway through the + * animation (if different from |aTo|) + * - toComp: Computed value version of |aTo| (if different from |aTo|) + * - noEffect: Special flag -- if set, indicates that this testcase is + * expected to have no effect on the computed value. (e.g. the + * given values are invalid.) + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + * + */ +function AnimTestcaseFromTo(aFrom, aTo, aComputedValMap, aSkipReason) +{ + this.from = aFrom; + this.to = aTo; + this.computedValMap = aComputedValMap || {}; // Let aComputedValMap be omitted + this.skipReason = aSkipReason; +} +AnimTestcaseFromTo.prototype = +{ + // Member variables + to : null, + + // Methods + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + // Call super, and then add my own customization + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(this, + [aAnimAttr, aTimeData, aIsFreeze]); + animElem.setAttribute("to", this.to) + return animElem; + }, +} +extend(AnimTestcaseFromTo, AnimTestcaseFrom); + +/* + * A testcase for a simple "from-by" animation. + * + * @param aFrom The 'from' value + * @param aBy The 'by' value + * @param aComputedValMap A hash-map that contains some computed values that + * we expect to visit, as follows: + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) + * - midComp: Computed value that we expect to visit halfway through the + * animation (|aFrom| + |aBy|/2) + * - toComp: Computed value of the animation endpoint (|aFrom| + |aBy|) + * - noEffect: Special flag -- if set, indicates that this testcase is + * expected to have no effect on the computed value. (e.g. the + * given values are invalid. Or the attribute may be animatable + * and additive, but the particular "from" & "by" values that + * are used don't support addition.) + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimTestcaseFromBy(aFrom, aBy, aComputedValMap, aSkipReason) +{ + this.from = aFrom; + this.by = aBy; + this.computedValMap = aComputedValMap; + this.skipReason = aSkipReason; + if (this.computedValMap && + !this.computedValMap.noEffect && !this.computedValMap.toComp) { + ok(false, "AnimTestcaseFromBy needs expected computed final value"); + } +} +AnimTestcaseFromBy.prototype = +{ + // Member variables + by : null, + + // Methods + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + // Call super, and then add my own customization + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply(this, + [aAnimAttr, aTimeData, aIsFreeze]); + animElem.setAttribute("by", this.by) + return animElem; + }, + buildSeekList : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + if (!aAnimAttr.isAdditive) { + return this.buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, + "defined as non-additive in SVG spec"); + } + // Just use inherited method + return AnimTestcaseFrom.prototype.buildSeekList.apply(this, + [aAnimAttr, aBaseVal, aTimeData, aIsFreeze]); + }, +} +extend(AnimTestcaseFromBy, AnimTestcaseFrom); + +/* + * A testcase for a "paced-mode" animation + * @param aValues An array of values, to be used as the "Values" list + * @param aComputedValMap A hash-map that contains some computed values, + * if they're needed, as follows: + * - comp0: The computed value at the start of the animation + * - comp1_6: The computed value exactly 1/6 through animation + * - comp1_3: The computed value exactly 1/3 through animation + * - comp2_3: The computed value exactly 2/3 through animation + * - comp1: The computed value of the animation endpoint + * The math works out easiest if... + * (a) aValuesString has 3 entries in its values list: vA, vB, vC + * (b) dist(vB, vC) = 2 * dist(vA, vB) + * With this setup, we can come up with expected intermediate values according + * to the following rules: + * - comp0 should be vA + * - comp1_6 should be us halfway between vA and vB + * - comp1_3 should be vB + * - comp2_3 should be halfway between vB and vC + * - comp1 should be vC + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimTestcasePaced(aValuesString, aComputedValMap, aSkipReason) +{ + this.valuesString = aValuesString; + this.computedValMap = aComputedValMap; + this.skipReason = aSkipReason; + if (this.computedValMap && + (!this.computedValMap.comp0 || + !this.computedValMap.comp1_6 || + !this.computedValMap.comp1_3 || + !this.computedValMap.comp2_3 || + !this.computedValMap.comp1)) { + ok(false, "This AnimTestcasePaced has an incomplete computed value map"); + } +} +AnimTestcasePaced.prototype = +{ + // Member variables + valuesString : null, + + // Methods + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + // Call super, and then add my own customization + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, + [aAnimAttr, aTimeData, aIsFreeze]); + animElem.setAttribute("values", this.valuesString) + animElem.setAttribute("calcMode", "paced"); + return animElem; + }, + buildSeekListAnimated : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + ": checking value "; + seekList.push([aTimeData.getBeginTime(), + this.computedValMap.comp0, + msgPrefix + "at start of animation"]); + seekList.push([aTimeData.getFractionalTime(1/6), + this.computedValMap.comp1_6, + msgPrefix + "1/6 of the way through animation."]); + seekList.push([aTimeData.getFractionalTime(1/3), + this.computedValMap.comp1_3, + msgPrefix + "1/3 of the way through animation."]); + seekList.push([aTimeData.getFractionalTime(2/3), + this.computedValMap.comp2_3, + msgPrefix + "2/3 of the way through animation."]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = this.computedValMap.comp1; + finalMsg = aAnimAttr.attrName + + ": [freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = aAnimAttr.attrName + + ": [remove-mode] checking that animation is cleared "; + } + seekList.push([aTimeData.getEndTime(), + expectedEndVal, finalMsg + "at end of animation"]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, finalMsg + "after end of animation"]); + return seekList; + }, + buildSeekListStatic : function(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) + { + var seekList = new Array(); + var msgPrefix = + aAnimAttr.attrName + ": shouldn't be affected by animation "; + seekList.push([aTimeData.getBeginTime(), aBaseVal, + msgPrefix + "(at animation begin) - " + aReasonStatic]); + seekList.push([aTimeData.getFractionalTime(1/6), aBaseVal, + msgPrefix + "(1/6 of the way through animation) - " + + aReasonStatic]); + seekList.push([aTimeData.getFractionalTime(1/3), aBaseVal, + msgPrefix + "(1/3 of the way through animation) - " + + aReasonStatic]); + seekList.push([aTimeData.getFractionalTime(2/3), aBaseVal, + msgPrefix + "(2/3 of the way through animation) - " + + aReasonStatic]); + seekList.push([aTimeData.getEndTime(), aBaseVal, + msgPrefix + "(at animation end) - " + aReasonStatic]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), aBaseVal, + msgPrefix + "(after animation end) - " + aReasonStatic]); + return seekList; + }, +}; +extend(AnimTestcasePaced, AnimTestcase); + +/* + * A testcase for an <animateMotion> animation. + * + * @param aAttrValueHash A hash-map mapping attribute names to values. + * Should include at least 'path', 'values', 'to' + * or 'by' to describe the motion path. + * @param aCtmMap A hash-map that contains summaries of the expected resulting + * CTM at various points during the animation. The CTM is + * summarized as a tuple of three numbers: [tX, tY, theta] + (indicating a translate(tX,tY) followed by a rotate(theta)) + * - ctm0: The CTM summary at the start of the animation + * - ctm1_6: The CTM summary at exactly 1/6 through animation + * - ctm1_3: The CTM summary at exactly 1/3 through animation + * - ctm2_3: The CTM summary at exactly 2/3 through animation + * - ctm1: The CTM summary at the animation endpoint + * + * NOTE: For paced-mode animation (the default for animateMotion), the math + * works out easiest if: + * (a) our motion path has 3 points: vA, vB, vC + * (b) dist(vB, vC) = 2 * dist(vA, vB) + * (See discussion in header comment for AnimTestcasePaced.) + * + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimMotionTestcase(aAttrValueHash, aCtmMap, aSkipReason) +{ + this.attrValueHash = aAttrValueHash; + this.ctmMap = aCtmMap; + this.skipReason = aSkipReason; + if (this.ctmMap && + (!this.ctmMap.ctm0 || + !this.ctmMap.ctm1_6 || + !this.ctmMap.ctm1_3 || + !this.ctmMap.ctm2_3 || + !this.ctmMap.ctm1)) { + ok(false, "This AnimMotionTestcase has an incomplete CTM map"); + } +} +AnimMotionTestcase.prototype = +{ + // Member variables + _animElementTagName : "animateMotion", + + // Implementations of inherited methods that we need to override: + // -------------------------------------------------------------- + setupAnimationElement : function(aAnimAttr, aTimeData, aIsFreeze) + { + var animElement = document.createElementNS(SVG_NS, + this._animElementTagName); + animElement.setAttribute("begin", aTimeData.getBeginTime()); + animElement.setAttribute("dur", aTimeData.getDur()); + if (aIsFreeze) { + animElement.setAttribute("fill", "freeze"); + } + for (var attrName in this.attrValueHash) { + if (attrName == "mpath") { + this.createPath(this.attrValueHash[attrName]); + this.createMpath(animElement); + } else { + animElement.setAttribute(attrName, this.attrValueHash[attrName]); + } + } + return animElement; + }, + + createPath : function(aPathDescription) + { + var path = document.createElementNS(SVG_NS, "path"); + path.setAttribute("d", aPathDescription); + path.setAttribute("id", MPATH_TARGET_ID); + return SMILUtil.getSVGRoot().appendChild(path); + }, + + createMpath : function(aAnimElement) + { + var mpath = document.createElementNS(SVG_NS, "mpath"); + mpath.setAttributeNS(XLINK_NS, "href", "#" + MPATH_TARGET_ID); + return aAnimElement.appendChild(mpath); + }, + + // Override inherited seekAndTest method since... + // (a) it expects a computedValMap and we have a computed-CTM map instead + // and (b) it expects we might have no effect (for non-animatable attrs) + buildSeekList : function(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) + { + var seekList = new Array(); + var msgPrefix = "CTM mismatch "; + seekList.push([aTimeData.getBeginTime(), + CTMUtil.generateCTM(this.ctmMap.ctm0), + msgPrefix + "at start of animation"]); + seekList.push([aTimeData.getFractionalTime(1/6), + CTMUtil.generateCTM(this.ctmMap.ctm1_6), + msgPrefix + "1/6 of the way through animation."]); + seekList.push([aTimeData.getFractionalTime(1/3), + CTMUtil.generateCTM(this.ctmMap.ctm1_3), + msgPrefix + "1/3 of the way through animation."]); + seekList.push([aTimeData.getFractionalTime(2/3), + CTMUtil.generateCTM(this.ctmMap.ctm2_3), + msgPrefix + "2/3 of the way through animation."]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = CTMUtil.generateCTM(this.ctmMap.ctm1); + finalMsg = aAnimAttr.attrName + + ": [freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = aAnimAttr.attrName + + ": [remove-mode] checking that animation is cleared "; + } + seekList.push([aTimeData.getEndTime(), + expectedEndVal, finalMsg + "at end of animation"]); + seekList.push([aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, finalMsg + "after end of animation"]); + return seekList; + }, + + // Override inherited seekAndTest method + // (Have to use assertCTMEqual() instead of is() for comparison, to check each + // component of the CTM and to allow for a small margin of error.) + seekAndTest : function(aSeekList, aTargetElem, aTargetAttr) + { + var svg = document.getElementById("svg"); + for (var i in aSeekList) { + var entry = aSeekList[i]; + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); + CTMUtil.assertCTMEqual(aTargetElem.getCTM(), entry[1], + CTMUtil.CTM_COMPONENTS_ALL, entry[2], false); + } + }, + + // Override "runTest" method so we can remove any <path> element that we + // created at the end of each test. + runTest : function(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) + { + AnimTestcase.prototype.runTest.apply(this, + [aTargetElem, aTargetAttr, aTimeData, aIsFreeze]); + var pathElem = document.getElementById(MPATH_TARGET_ID); + if (pathElem) { + SMILUtil.getSVGRoot().removeChild(pathElem); + } + } +}; +extend(AnimMotionTestcase, AnimTestcase); + +// MAIN METHOD +function testBundleList(aBundleList, aTimingData) +{ + for (var bundleIdx in aBundleList) { + aBundleList[bundleIdx].go(aTimingData); + } +} diff --git a/dom/smil/test/smilXHR_helper.svg b/dom/smil/test/smilXHR_helper.svg new file mode 100644 index 000000000..cb0b51c36 --- /dev/null +++ b/dom/smil/test/smilXHR_helper.svg @@ -0,0 +1,8 @@ +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle id="circ" cx="20" cy="20" r="15" fill="blue"> + <animate id="animXML" attributeName="cx" attributeType="XML" + from="500" to="600" begin="0s" dur="4s"/> + <animate id="animCSS" attributeName="opacity" attributeType="CSS" + from="0.2" to="0.3" begin="0s" dur="4s"/> + </circle> +</svg> diff --git a/dom/smil/test/test_smilAccessKey.xhtml b/dom/smil/test/test_smilAccessKey.xhtml new file mode 100644 index 000000000..ef7e1b73d --- /dev/null +++ b/dom/smil/test/test_smilAccessKey.xhtml @@ -0,0 +1,362 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL accessKey support</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=587910">Mozilla Bug + 587910</a> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="20" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL accessKey support **/ + +const gSvgns = "http://www.w3.org/2000/svg"; +var gSvg = document.getElementById("svg"); +SimpleTest.waitForExplicitFinish(); + +function main() +{ + gSvg.pauseAnimations(); + + // Basic syntax + testOk('accessKey(a)', 'a'); + testOk(' accessKey(a) ', 'a'); + testNotOk('accessKey (a)', 'a'); + testNotOk('accessKey( a)', 'a'); + testNotOk('accessKey(a )', 'a'); + testNotOk('accessKey(a)', 'b'); + testNotOk('accessKey()', ' '); + + // Test the test framework itself + testOk('accessKey(a)', 97); + + // Allow for either accessKey (SVG / SMIL Animation) or accesskey (SMIL2+) + testOk('accesskey(a)', 'a'); + + // Offset + testOk('accessKey(a)+0s', 'a'); + testOk('accessKey(a) + 0min', 'a'); + testOk('accessKey(a) -0h', 'a'); + testOk('accessKey(a)+100ms', 'a', 0, 0.1); + testOk('accessKey(a)-0.1s', 'a', 0, -0.1); + + // Id references are not allowed + testNotOk('svg.accessKey(a)', 'a'); + testNotOk('window.accessKey(a)', 'a'); + + // Case sensitivity + testOk('accessKey(A)', 'A'); + testNotOk('accessKey(a)', 'A'); + testNotOk('accessKey(A)', 'a'); + + // Test unusual characters + testOk('accessKey(-)', '-'); + testOk('accessKey(\\)', '\\'); + testOk('accessKey( )', ' '); + testOk('accessKey(\x0D)', 0, KeyboardEvent.DOM_VK_RETURN); + testOk('accessKey(\n)', 0, KeyboardEvent.DOM_VK_RETURN); // New line + testOk('accessKey(\r)', 0, KeyboardEvent.DOM_VK_RETURN); // Carriage return + testOk('accessKey(\x08)', 0, KeyboardEvent.DOM_VK_BACK_SPACE); + testOk('accessKey(\x1B)', 0, KeyboardEvent.DOM_VK_ESCAPE); + testOk('accessKey(\x7F)', 0, KeyboardEvent.DOM_VK_DELETE); + + // Check some disallowed keys + // -- For now we don't allow tab since the interaction with focus causes + // confusing results + testNotOk('accessKey(\x09)', 0, 9); // Tab + + // Test setting the keyCode field + testNotOk('accessKey(a)', 0, 97); + testOk('accessKey(a)', 97, 66); // Give priority to charCode field + testNotOk('accessKey(a)', 98, 97); // Give priority to charCode field + + // Test unicode + testOk("accessKey(\u20AC)", 8364); // euro-symbol + + // Test an astral character just to make sure we don't crash + testOk("accessKey(\uD835\uDC00)", 119808); // mathematical bold capital A + // 0x1D400 + // Test bad surrogate pairs don't confuse us either + testNotOk("accessKey(\uD800\uD800)", 97); + testNotOk("accessKey(\uD80020)", 97); + testNotOk("accessKey(\uD800)", 97); + + // Test modifiers + // -- When matching on charCode ignore shift and alt + testNotOk('accessKey(a)', 'a', 0, 0, { ctrl: true }); + testNotOk('accessKey(a)', 'a', 0, 0, { meta: true }); + testOk('accessKey(a)', 'a', 0, 0, { alt: true }); + testOk('accessKey(a)', 'a', 0, 0, { shift: true }); + testNotOk('accessKey(a)', 'a', 0, 0, { shift: true, ctrl: true }); + testNotOk('accessKey(a)', 'a', 0, 0, { alt: true, meta: true }); + // -- When matching on keyCode ignore all + testNotOk('accessKey(\x0D)', 0, 13, 0, { ctrl: true }); + testNotOk('accessKey(\x0D)', 0, 13, 0, { meta: true }); + testNotOk('accessKey(\x0D)', 0, 13, 0, { alt: true }); + testNotOk('accessKey(\x0D)', 0, 13, 0, { shift: true }); + testNotOk('accessKey(\x0D)', 0, 13, 0, { shift: true, ctrl: true }); + + testOpenEnd(); + testPreventDefault(); + testDispatchToWindow(); + testAdoptNode(); + testFauxEvent(); + + SimpleTest.finish(); +} + +function testOk(spec, charCode, keyCode, offset, modifiers) +{ + if (typeof offset == 'undefined') offset = 0; + var msg = "No interval created for '" + spec + + "' with input [charCode: " + charCode + "; keyCode: " + keyCode + "]" + + getModifiersDescr(modifiers); + ok(test(spec, charCode, keyCode, offset, modifiers), msg); +} + +function testNotOk(spec, charCode, keyCode, offset, modifiers) +{ + if (typeof offset == 'undefined') offset = 0; + var msg = "Interval unexpectedly created for '" + spec + + "' with input [charCode: " + charCode + "; keyCode: " + keyCode + "]" + + getModifiersDescr(modifiers); + ok(!test(spec, charCode, keyCode, offset, modifiers), msg); +} + +function getModifiersDescr(modifiers) +{ + if (typeof modifiers != 'object') + return ''; + var str = ' modifiers set:'; + for (var key in modifiers) { + if (modifiers[key]) str += ' ' + key; + } + return str; +} + +function test(spec, charCode, keyCode, offset, modifiers) +{ + gSvg.setCurrentTime(1); + ok(gSvg.animationsPaused(), "Expected animations to be paused"); + + var anim = createAnim(spec); + var evt = createEvent(charCode, keyCode, modifiers); + + document.getElementById('circle').dispatchEvent(evt); + + var gotStartTimeOk = true; + try { + var start = anim.getStartTime(); + if (offset) { + var expected = gSvg.getCurrentTime() + offset; + ok(Math.abs(expected - start) <= 0.00001, + "Unexpected start time for animation with begin: " + spec + + " got " + start + ", expected " + expected); + } else { + is(start, gSvg.getCurrentTime() + offset, + "Unexpected start time for animation with begin: " + spec); + } + } catch(e) { + is(e.name, "InvalidStateError", + "Unexpected exception: " + e.name); + is(e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code: " + e.code); + gotStartTimeOk = false; + } + + anim.parentNode.removeChild(anim); + + return gotStartTimeOk; +} + +function createAnim(beginSpec) +{ + var anim = document.createElementNS(gSvgns, 'animate'); + anim.setAttribute('attributeName', 'cx'); + anim.setAttribute('values', '0; 100'); + anim.setAttribute('dur', '10s'); + anim.setAttribute('begin', beginSpec); + return document.getElementById('circle').appendChild(anim); +} + +function createEvent(charCode, keyCode, modifiers) +{ + if (typeof charCode == 'string') { + is(charCode.length, 1, + "If charCode is a string it should be 1 character long"); + charCode = charCode.charCodeAt(0); + } else if (typeof charCode == 'undefined') { + charCode = 0; + } + args = { ctrl: false, alt: false, shift: false, meta: false }; + if (typeof modifiers == 'object') { + for (var key in modifiers) + args[key] = modifiers[key]; + } + if (typeof keyCode == 'undefined') keyCode = 0; + var evt = document.createEvent("KeyboardEvent"); + evt.initKeyEvent("keypress", true, true, window, + args['ctrl'], + args['alt'], + args['shift'], + args['meta'], + keyCode, + charCode); + return evt; +} + +function testOpenEnd() +{ + // Test that an end specification with an accesskey value is treated as open + // ended + gSvg.setCurrentTime(0); + ok(gSvg.animationsPaused(), "Expected animations to be paused"); + + var anim = createAnim('0s; 2s'); + anim.setAttribute('end', '1s; accessKey(a)'); + + gSvg.setCurrentTime(2); + + try { + is(anim.getStartTime(), 2, + "Unexpected start time for second interval of open-ended animation"); + } catch(e) { + is(e.name, "InvalidStateError", + "Unexpected exception:" + e.name); + is(e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code:" + e.code); + ok(false, "Failed to recognise accessKey as qualifying for creating an " + + "open-ended interval"); + } + + anim.parentNode.removeChild(anim); +} + +function testPreventDefault() +{ + // SVG/SMIL don't specify what should happen if preventDefault is called on + // the keypress event. For now, for consistency with event timing we ignore + // it. + gSvg.setCurrentTime(1); + ok(gSvg.animationsPaused(), "Expected animations to be paused"); + + var anim = createAnim('accessKey(a)'); + var evt = createEvent('a'); + + var circle = document.getElementById('circle'); + var func = function(evt) { evt.preventDefault(); } + circle.addEventListener('keypress', func, false); + circle.dispatchEvent(evt); + + try { + var start = anim.getStartTime(); + } catch(e) { + ok(false, "preventDefault() cancelled accessKey handling"); + } + + circle.removeEventListener('keypress', func, false); + anim.parentNode.removeChild(anim); +} + +function testDispatchToWindow() +{ + gSvg.setCurrentTime(1); + ok(gSvg.animationsPaused(), "Expected animations to be paused"); + + var anim = createAnim('accessKey(a)'); + var evt = createEvent('a'); + + window.dispatchEvent(evt); + + try { + var start = anim.getStartTime(); + } catch(e) { + ok(false, "Key event dispatched to the window failed to trigger " + + "accesskey handling"); + } + + anim.parentNode.removeChild(anim); +} + +function testAdoptNode() +{ + gSvg.setCurrentTime(1); + ok(gSvg.animationsPaused(), "Expected animations to be paused"); + + // Create a new document with an animation element + var newdoc = document.implementation.createDocument(gSvgns, 'svg', null); + var anim = newdoc.createElementNS(gSvgns, 'animate'); + anim.setAttribute('attributeName', 'cx'); + anim.setAttribute('values', '0; 100'); + anim.setAttribute('dur', '10s'); + anim.setAttribute('begin', 'accesskey(a)'); + newdoc.documentElement.appendChild(anim); + + // Adopt + ok(anim.ownerDocument !== document, + "Expected newly created animation to belong to a different doc"); + document.adoptNode(anim); + document.getElementById('circle').appendChild(anim); + ok(anim.ownerDocument === document, + "Expected newly created animation to belong to the same doc"); + + var evt = createEvent('a'); + + // Now fire an event at the original window and check nothing happens + newdoc.dispatchEvent(evt); + try { + var start = anim.getStartTime(); + ok(false, "Adopted node still receiving accesskey events from old doc"); + } catch(e) { + // Ok + } + + // And then fire at our window + document.dispatchEvent(evt); + try { + var start = anim.getStartTime(); + } catch(e) { + ok(false, "Adopted node failed to catch accesskey event"); + } + + anim.parentNode.removeChild(anim); +} + +function testFauxEvent() +{ + // Test a non-KeyEvent labelled as a key event + gSvg.setCurrentTime(0); + ok(gSvg.animationsPaused(), "Expected animations to be paused"); + + var anim = createAnim('accessKey(a)'); + var evt = document.createEvent("SVGEvents"); + evt.initEvent("keypress", true, true); + document.getElementById('circle').dispatchEvent(evt); + + // We're really just testing that the above didn't crash us, but while we're + // at it, just do a sanity check that we didn't also create an interval + try { + var start = anim.getStartTime(); + ok(false, "Faux event generated interval"); + } catch(e) { + // All is well + } + + anim.parentNode.removeChild(anim); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilAnimateMotion.xhtml b/dom/smil/test/test_smilAnimateMotion.xhtml new file mode 100644 index 000000000..250c0b80f --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotion.xhtml @@ -0,0 +1,51 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=436418 +--> +<head> + <title>Test for animateMotion behavior</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilAnimateMotion.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436418">Mozilla Bug 436418</a> +<p id="display"></p> +<div id="content" style="visibility: hidden"> + +<!-- NOTE: Setting font-size so we can test 'em' units --> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" style="font-size: 500px" + onload="this.pauseAnimations()"> + <!-- XXXdholbert Right now, 'em' conversions are correct if we set font-size + on rect using the inline style attr. However, if we use 'font-size' attr, + then 'em' units end up using the inherited font-size instead. Bug? --> + <rect x="20" y="20" width="200" height="200" style="font-size: 10px"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var timingData = new SMILTimingData(1.0, 6.0); + testBundleList(gMotionBundles, timingData); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml b/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml new file mode 100644 index 000000000..0554c7fd8 --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml @@ -0,0 +1,176 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=436418 +--> +<head> + <title>Test for animateMotion acceptance of invalid values</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js" /> + <script type="text/javascript" src="smilAnimateMotionValueLists.js" /> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436418">Mozilla Bug 436418</a> +<p id="display"></p> +<div id="content" style="visibility: hidden"> +<svg xmlns="http://www.w3.org/2000/svg" id="svg" + width="200px" height="200px" + onload="this.pauseAnimations()"> + <rect id="rect" x="20" y="20" width="200" height="200"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +// Constant strings (& string-arrays) +const SVGNS = "http://www.w3.org/2000/svg"; +const XLINKNS = "http://www.w3.org/1999/xlink"; + +// Constant objects +const gSvg = document.getElementById("svg"); +const gRect = document.getElementById("rect"); +const gUnAnimatedCTM = gRect.getCTM(); + +SimpleTest.waitForExplicitFinish(); + +function createAnim() +{ + var anim = document.createElementNS(SVGNS, "animateMotion"); + anim.setAttribute("dur", "2s"); + return gRect.appendChild(anim); +} + +function removeElem(aElem) +{ + aElem.parentNode.removeChild(aElem); +} + +function testAttr(aAttrName, aAttrValueArray, aIsValid) +{ + var componentsToCheck; + + for (var i in aAttrValueArray) { + var curVal = aAttrValueArray[i]; + var anim = createAnim(); + anim.setAttribute(aAttrName, curVal); + if (aAttrName == "rotate") { + // Apply a diagonal translation (so rotate='auto' will have an effect) + // and just test the rotation matrix components + anim.setAttribute("values", "0 0; 50 50"); + componentsToCheck = CTMUtil.CTM_COMPONENTS_ROTATE; + } else { + // Apply a supplementary rotation to make sure that we don't apply it if + // our value is rejected. + anim.setAttribute("rotate", Math.PI/4); + componentsToCheck = CTMUtil.CTM_COMPONENTS_ALL; + if (aAttrName == "keyPoints") { + // Add three times so we can test a greater range of values for + // keyPoints + anim.setAttribute("values", "0 0; 25 25; 50 50"); + anim.setAttribute("keyTimes", "0; 0.5; 1"); + anim.setAttribute("calcMode", "discrete"); + } + } + + var curCTM = gRect.getCTM(); + if (aIsValid) { + var errMsg = "CTM should have changed when applying animateMotion " + + "with '" + aAttrName + "' set to valid value '" + curVal + "'"; + CTMUtil.assertCTMNotEqual(curCTM, gUnAnimatedCTM, componentsToCheck, + errMsg, false); + } else { + var errMsg = "CTM should not have changed when applying animateMotion " + + "with '" + aAttrName + "' set to invalid value '" + curVal + "'"; + CTMUtil.assertCTMEqual(curCTM, gUnAnimatedCTM, componentsToCheck, + errMsg, false); + } + removeElem(anim); + } +} + +function createPath(aPathDescription) +{ + var path = document.createElementNS(SVGNS, "path"); + path.setAttribute("d", aPathDescription); + path.setAttribute("id", "thePath"); + return gSvg.appendChild(path); +} + +function createMpath(aAnimElement) +{ + var mpath = document.createElementNS(SVGNS, "mpath"); + mpath.setAttributeNS(XLINKNS, "href", "#thePath"); + return aAnimElement.appendChild(mpath); +} + +function testMpathElem(aPathValueArray, aIsValid) +{ + for (var i in aPathValueArray) { + var curVal = aPathValueArray[i]; + var anim = createAnim(); + var mpath = createMpath(anim); + var path = createPath(curVal); + + // Apply a supplementary rotation to make sure that we don't apply it if + // our value is rejected. + anim.setAttribute("rotate", Math.PI/4); + componentsToCheck = CTMUtil.CTM_COMPONENTS_ALL; + + if (aIsValid) { + var errMsg = "CTM should have changed when applying animateMotion " + + "with mpath linking to a path with valid value '" + curVal + "'"; + + CTMUtil.assertCTMNotEqual(gRect.getCTM(), gUnAnimatedCTM, + componentsToCheck, errMsg, false); + } else { + var errMsg = "CTM should not have changed when applying animateMotion " + + "with mpath linking to a path with invalid value '" + curVal + "'"; + CTMUtil.assertCTMEqual(gRect.getCTM(), gUnAnimatedCTM, + componentsToCheck, errMsg, false); + } + removeElem(anim); + removeElem(path); + removeElem(mpath); + } +} + +// Main Function +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testAttr("values", gValidValues, true); + testAttr("values", gInvalidValues, false); + + testAttr("rotate", gValidRotate, true); + testAttr("rotate", gInvalidRotate, false); + + testAttr("to", gValidToBy, true); + testAttr("to", gInvalidToBy, false); + + testAttr("by", gValidToBy, true); + testAttr("by", gInvalidToBy, false); + + testAttr("path", gValidPath, true); + testAttr("path", gInvalidPath, false); + testAttr("path", gValidPathWithErrors, true); + + testAttr("keyPoints", gValidKeyPoints, true); + testAttr("keyPoints", gInvalidKeyPoints, false); + + testMpathElem(gValidPath, true); + testMpathElem(gInvalidPath, false); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml b/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml new file mode 100644 index 000000000..a428ff332 --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml @@ -0,0 +1,215 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=436418 +--> +<head> + <title>Test for overriding of path-defining attributes for animateMotion</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js" /> + <script type="text/javascript" src="smilAnimateMotionValueLists.js" /> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436418">Mozilla Bug 436418</a> +<p id="display"></p> +<div id="content" style="visibility: hidden"> +<svg xmlns="http://www.w3.org/2000/svg" id="svg" + width="200px" height="200px" + onload="this.pauseAnimations()"> + <!-- Paths for mpath to refer to --> + <path id="validPathElem" d="M10 10 h-10"/> + <path id="invalidPathElem" d="abc"/> + + <!-- The rect whose motion is animated --> + <rect id="rect" x="20" y="20" width="200" height="200"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +// Constant strings (& string-arrays) +const SVGNS = "http://www.w3.org/2000/svg"; +const XLINKNS = "http://www.w3.org/1999/xlink"; + +// Constant objects +const gSvg = document.getElementById("svg"); +const gRect = document.getElementById("rect"); +const gUnAnimatedCTM = gRect.getCTM(); + +// Values for path-defining attributes, and their expected +// CTMs halfway through the animation +var gMpathValidTarget = "#validPathElem"; +var gMpathCTM = CTMUtil.generateCTM([ 5, 10, 0 ]); + +var gMpathInvalidTargetA = "#invalidPathElem"; +var gMpathInvalidTargetB = "#nonExistentElem"; + +var gInvalidAttrValue = "i-am-invalid"; // Invalid for all tested attributes + +var gPathValidValue = "M20 20 h10"; +var gPathCTM = CTMUtil.generateCTM([ 25, 20, 0 ]); + +var gValuesValidValue = "30 30; 40 30" +var gValuesCTM = CTMUtil.generateCTM([ 35, 30, 0 ]); + +var gFromValidValue = "50 50"; + +var gByValidValue = "10 2"; +var gPureByCTM = CTMUtil.generateCTM([ 5, 1, 0 ]); +var gFromByCTM = CTMUtil.generateCTM([ 55, 51, 0 ]); + +var gToValidValue = "80 60"; +var gPureToCTM = CTMUtil.generateCTM([ 40, 30, 0 ]); +var gFromToCTM = CTMUtil.generateCTM([ 65, 55, 0 ]); + + +SimpleTest.waitForExplicitFinish(); + +function createAnim() +{ + var anim = document.createElementNS(SVGNS, "animateMotion"); + return gRect.appendChild(anim); +} + +function removeElem(aElem) +{ + aElem.parentNode.removeChild(aElem); +} + +function createMpath(aAnimElement, aHrefVal) +{ + var mpath = document.createElementNS(SVGNS, "mpath"); + mpath.setAttributeNS(XLINKNS, "href", aHrefVal); + return aAnimElement.appendChild(mpath); +} + +function runTest() { + // Start out with valid values for all path-defining attributes + var attrSettings = { + "mpath" : gMpathValidTarget, + "path" : gPathValidValue, + "values" : gValuesValidValue, + "from" : gFromValidValue, + "to" : gToValidValue, + "by" : gByValidValue, + }; + + // Test that <mpath> overrides everything below it + testAttrSettings(attrSettings, gMpathCTM, + "<mpath> should win"); + var mpathInvalidTargets = [gMpathInvalidTargetA, gMpathInvalidTargetB]; + for (var i in mpathInvalidTargets) { + var curInvalidValue = mpathInvalidTargets[i]; + attrSettings["mpath"] = curInvalidValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid <mpath> should block animation"); + } + delete attrSettings["mpath"]; + + // Test that 'path' overrides everything below it + testAttrSettings(attrSettings, gPathCTM, + "'path' should win vs all but mpath"); + attrSettings["path"] = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'path' should block animation vs all but mpath"); + delete attrSettings["path"]; + + // Test that 'values' overrides everything below it + testAttrSettings(attrSettings, gValuesCTM, + "'values' should win vs from/by/to"); + attrSettings["values"] = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'values' should block animation vs from/by/to"); + delete attrSettings["values"]; + + // Test that 'from' & 'to' overrides 'by' + testAttrSettings(attrSettings, gFromToCTM, + "'from/to' should win vs 'by'"); + attrSettings["to"] = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'to' should block animation vs 'by'"); + delete attrSettings["to"]; + + // Test that 'from' & 'by' are effective + testAttrSettings(attrSettings, gFromByCTM, + "'from/by' should be visible"); + attrSettings["by"] = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'by' should block animation"); + delete attrSettings["from"]; + + // REINSERT "to" & fix up "by" so we can test pure-"to" vs pure-"by" + attrSettings["to"] = gToValidValue; + attrSettings["by"] = gByValidValue; + testAttrSettings(attrSettings, gPureToCTM, + "pure-'to' should be effective & beat pure-'by'"); + attrSettings["to"] = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid pure-'to' should block animation vs pure-'by'"); + delete attrSettings["to"]; + + // Test that pure-"by" is effective + testAttrSettings(attrSettings, gPureByCTM, + "pure-by should be visible"); + attrSettings["by"] = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'by' should block animation"); + delete attrSettings["by"]; + + // Make sure that our hash is empty now. + for (var unexpectedKey in attrSettings) { + ok(false, "Unexpected mapping remains in attrSettings: " + + unexpectedKey + "-->" + unexpectedValue); + } +} + +function testAttrSettings(aAttrValueHash, aExpectedCTM, aErrMsg) +{ + var isDebug = false; // XXdholbert + !isDebug || todo(false, "ENTERING testAttrSettings"); + // Set up animateMotion element + var animElement = document.createElementNS(SVGNS, "animateMotion"); + animElement.setAttribute("dur", "2s"); + for (var attrName in aAttrValueHash) { + !isDebug || todo(false, "setting '" + attrName +"' to '" + + aAttrValueHash[attrName] +"'"); + if (attrName == "mpath") { + createMpath(animElement, aAttrValueHash[attrName]); + } else { + animElement.setAttribute(attrName, aAttrValueHash[attrName]); + } + } + + gRect.appendChild(animElement); + + // Seek to halfway through animation + SMILUtil.getSVGRoot().setCurrentTime(1); // Seek halfway through animation + + // Check CTM against expected value + CTMUtil.assertCTMEqual(gRect.getCTM(), aExpectedCTM, + CTMUtil.CTM_COMPONENTS_ALL, aErrMsg, false); + + // CLEAN UP + SMILUtil.getSVGRoot().setCurrentTime(0); + removeElem(animElement); +} + +// Main Function +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + runTest(); + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilBackwardsSeeking.xhtml b/dom/smil/test/test_smilBackwardsSeeking.xhtml new file mode 100644 index 000000000..7a40bf718 --- /dev/null +++ b/dom/smil/test/test_smilBackwardsSeeking.xhtml @@ -0,0 +1,191 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for backwards seeking behavior </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" /> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for backwards seeking behavior **/ + +var gSvg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Pause our document, so that the setCurrentTime calls are the only + // thing affecting document time + gSvg.pauseAnimations(); + + // We define a series of scenarios, sample times, and expected return values + // from getStartTime. + // + // Each scenario is basically a variation on the following arrangement: + // + // <svg> + // <set ... dur="1s" begin="<A-BEGIN>"/> + // <set ... dur="1s" begin="<B-BEGIN>"/> + // </svg> + // + // Each test then consists of the following: + // animA: attributes to be applied to a + // animB: attributes to be applied to b + // times: a series of triples which consist of: + // <sample time, a's expected start time, b's expected start time> + // * The sample time is the time passed to setCurrentTime and so is + // in seconds. + // * The expected start times are compared with the return value of + // getStartTime. To check for an unresolved start time where + // getStartTime would normally throw an exception use + // 'unresolved'. + // * We also allow the special notation to indicate a call to + // beginElement + // <'beginElementAt', id of animation element, offset> + // + // In the diagrams below '^' means the time before the seek and '*' is the + // seek time. + var testCases = Array(); + + // 0: Simple case + // + // A: +------- + // B: +------- begin: a.begin + // * ^ + testCases[0] = { + 'animA': {'begin':'1s', 'id':'a'}, + 'animB': {'begin':'a.begin'}, + 'times': [ [0, 1, 1], + [1, 1, 1], + [2, 'unresolved', 'unresolved'], + [0, 1, 1], + [1.5, 1, 1], + [1, 1, 1], + [2, 'unresolved', 'unresolved'] ] + }; + + // 1: Restored times should be live + // + // When we restore times they should be live. So we have the following + // scenario. + // + // A: +------- + // B: +------- begin: a.begin + // * ^ + // + // Then we call beginElement at an earlier time which should give us the + // following. + // + // A: +------- + // B: +------- + // * ^ + // + // If the times are not live however we'll end up with this + // + // A: +------- + // B: +-+------- + // * ^ + testCases[1] = { + 'animA': {'begin':'1s', 'id':'a', 'restart':'whenNotActive'}, + 'animB': {'begin':'a.begin', 'restart':'always'}, + 'times': [ [0, 1, 1], + [2, 'unresolved', 'unresolved'], + [0.25, 1, 1], + ['beginElementAt', 'a', 0.25], // = start time of 0.5 + [0.25, 0.5, 0.5], + [1, 0.5, 0.5], + [1.5, 'unresolved', 'unresolved'] ] + }; + + // 2: Multiple intervals A + // + // A: +- +- + // B: +- +- begin: a.begin+4s + // * ^ + testCases[2] = { + 'animA': {'begin':'1s; 3s', 'id':'a'}, + 'animB': {'begin':'a.begin+4s'}, + 'times': [ [0, 1, 5], + [3, 3, 5], + [6.5, 'unresolved', 7], + [4, 'unresolved', 5], + [6, 'unresolved', 7], + [2, 3, 5], + ['beginElementAt', 'a', 0], + [2, 2, 5], + [5, 'unresolved', 5], + [6, 'unresolved', 6], + [7, 'unresolved', 7], + [8, 'unresolved', 'unresolved'] ] + }; + + for (var i = 0; i < testCases.length; i++) { + gSvg.setCurrentTime(0); + var test = testCases[i]; + + // Create animation elements + var animA = createAnim(test.animA); + var animB = createAnim(test.animB); + + // Run samples + for (var j = 0; j < test.times.length; j++) { + var times = test.times[j]; + if (times[0] == 'beginElementAt') { + var anim = getElement(times[1]); + anim.beginElementAt(times[2]); + } else { + gSvg.setCurrentTime(times[0]); + checkStartTime(animA, times[1], times[0], i, 'a'); + checkStartTime(animB, times[2], times[0], i, 'b'); + } + } + + // Tidy up + animA.parentNode.removeChild(animA); + animB.parentNode.removeChild(animB); + } + + SimpleTest.finish(); +} + +function createAnim(attr) +{ + const svgns = "http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns, 'set'); + anim.setAttribute('attributeName','x'); + anim.setAttribute('to','10'); + anim.setAttribute('dur','1s'); + for (name in attr) { + anim.setAttribute(name, attr[name]); + } + return gSvg.appendChild(anim); +} + +function checkStartTime(anim, expectedStartTime, sampleTime, caseNum, id) +{ + var startTime = 'unresolved'; + try { + startTime = anim.getStartTime(); + } catch (e) { + if (e.name != "InvalidStateError" || + e.code != DOMException.INVALID_STATE_ERR) + throw e; + } + + var msg = "Test case " + caseNum + ", t=" + sampleTime + " animation '" + + id + "': Unexpected getStartTime:"; + is(startTime, expectedStartTime, msg); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSFontStretchRelative.xhtml b/dom/smil/test/test_smilCSSFontStretchRelative.xhtml new file mode 100644 index 000000000..08caff267 --- /dev/null +++ b/dom/smil/test/test_smilCSSFontStretchRelative.xhtml @@ -0,0 +1,102 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg"> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/* This testcase verifies that animated values of "wider" and "narrower" for + "font-stretch" have the expected effect, across all possible inherited + values of the property. + XXXdholbert Currently, we don't support animating relative values of + font-stretch, so most of the tests here use todo_is() rather than is(). +*/ + +SimpleTest.waitForExplicitFinish(); + +const gPropertyName="font-stretch"; + +// List of non-relative font-stretch values, from smallest to largest +const gFontStretchValues = [ + "ultra-condensed", + "extra-condensed", + "condensed", + "semi-condensed", + "normal", + "semi-expanded", + "expanded", + "extra-expanded", + "ultra-expanded" +]; + +function testFontStretchValue(baseValue, narrowerStep, widerStep) +{ + var svg = SMILUtil.getSVGRoot(); + var gElem = document.createElementNS(SVG_NS, "g"); + gElem.setAttribute("style", "font-stretch: " + baseValue); + svg.appendChild(gElem); + + var textElem = document.createElementNS(SVG_NS, "text"); + gElem.appendChild(textElem); + + var animElem = document.createElementNS(SVG_NS, "set"); + animElem.setAttribute("attributeName", gPropertyName); + animElem.setAttribute("attributeType", "CSS"); + animElem.setAttribute("begin", "0s"); + animElem.setAttribute("dur", "indefinite"); + textElem.appendChild(animElem); + + // CHECK EFFECT OF 'narrower' + // NOTE: Using is() instead of todo_is() for ultra-condensed, since + // 'narrower' has no effect on that value. + var myIs = (baseValue == "ultra-condensed" ? is : todo_is); + animElem.setAttribute("to", "narrower"); + SMILUtil.getSVGRoot().setCurrentTime(1.0); // Force a resample + myIs(SMILUtil.getComputedStyleSimple(textElem, gPropertyName), narrowerStep, + "checking effect of 'narrower' on inherited value '" + baseValue + "'"); + + // CHECK EFFECT OF 'wider' + // NOTE: using is() instead of todo_is() for ultra-expanded, since + // 'wider' has no effect on that value. + myIs = (baseValue == "ultra-expanded" ? is : todo_is); + animElem.setAttribute("to", "wider"); + SMILUtil.getSVGRoot().setCurrentTime(1.0); // Force a resample + myIs(SMILUtil.getComputedStyleSimple(textElem, gPropertyName), widerStep, + "checking effect of 'wider' on inherited value '" + baseValue + "'"); + + // Removing animation should clear animated effects + textElem.removeChild(animElem); + svg.removeChild(gElem); +} + +function main() +{ + var valuesList = gFontStretchValues; + for (var baseIdx in valuesList) { + // 'narrower' and 'wider' are expected to shift us by one slot, but not + // past the ends of the list of possible values. + var narrowerIdx = Math.max(baseIdx - 1, 0); + var widerIdx = Math.min(baseIdx + 1, valuesList.length - 1); + + testFontStretchValue(valuesList[baseIdx], + valuesList[narrowerIdx], valuesList[widerIdx]); + } + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSFromBy.xhtml b/dom/smil/test/test_smilCSSFromBy.xhtml new file mode 100644 index 000000000..d6ac7ff0e --- /dev/null +++ b/dom/smil/test/test_smilCSSFromBy.xhtml @@ -0,0 +1,49 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSFromBy.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is + more predictable. (otherwise, line-height varies depending on platform) + --> + <text x="20" y="20" style="line-height: 10px !important">testing 123</text> + <line/> + <marker/> + <filter><feDiffuseLighting/></filter> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testBundleList(gFromByBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSFromTo.xhtml b/dom/smil/test/test_smilCSSFromTo.xhtml new file mode 100644 index 000000000..88f3ad715 --- /dev/null +++ b/dom/smil/test/test_smilCSSFromTo.xhtml @@ -0,0 +1,76 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSFromTo.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is + more predictable. (otherwise, line-height varies depending on platform) + --> + <text x="20" y="20" style="line-height: 10px !important">testing 123</text> + <line/> + <image/> + <marker/> + <clipPath><circle/></clipPath> + <filter><feFlood/></filter> + <filter><feDiffuseLighting/></filter> + <linearGradient><stop/></linearGradient> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function checkForUntestedProperties(bundleList) +{ + // Create the set of all the properties we know about + var propertySet = {}; + for (propertyLabel in gPropList) { + // insert property + propertySet[gPropList[propertyLabel].attrName] = null; + } + // Remove tested properties from the set + for (var bundleIdx in bundleList) { + var bundle = bundleList[bundleIdx]; + delete propertySet[bundle.animatedAttribute.attrName]; + } + // Warn about remaining (untested) properties + for (var untestedProp in propertySet) { + ok(false, "No tests for property '" + untestedProp + "'"); + } +} + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // FIRST: Warn about any properties that are missing tests + checkForUntestedProperties(gFromToBundles); + + // Run the actual tests + testBundleList(gFromToBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSInherit.xhtml b/dom/smil/test/test_smilCSSInherit.xhtml new file mode 100644 index 000000000..9da18f52b --- /dev/null +++ b/dom/smil/test/test_smilCSSInherit.xhtml @@ -0,0 +1,85 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="300px" height="200px" + onload="this.pauseAnimations()"> + <!-- At 50% through the animation, the following should be true: + * First <g> has font-size = 5px (1/2 between 0px and 10px) + * Next <g> has font-size = 10px (1/2 between inherit=5px and 15px) + * Next <g> has font-size = 15px (1/2 between inherit=10px and 20px) + * Next <g> has font-size = 20px (1/2 between inherit=15px and 25px) + * Next <g> has font-size = 25px (1/2 between inherit=20px and 30px) + * Next <g> has font-size = 30px (1/2 between inherit=25px and 35px) + * Next <g> has font-size = 35px (1/2 between inherit=30px and 40px) + * Next <g> has font-size = 40px (1/2 between inherit=35px and 45px) + * Next <g> has font-size = 45px (1/2 between inherit=40px and 50px) + * Next <g> has font-size = 50px (1/2 between inherit=45px and 55px) + * <text> has font-size = 75px (1/2 between inherit=50px and 100px) + --> + <g><animate attributeName="font-size" attributeType="CSS" + from="0px" to="10px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="15px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="20px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="25px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="30px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="35px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="40px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="45px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="50px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="55px" begin="0s" dur="1s"/> + <text y="100px" x="0px"> + abc + <animate attributeName="font-size" attributeType="CSS" + from="inherit" to="100px" begin="0s" dur="1s"/> + </text></g></g></g></g></g></g></g></g></g></g> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +function main() { + // Pause & seek to halfway through animation + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + svg.setCurrentTime(0.5); + + var text = document.getElementsByTagName("text")[0]; + var computedVal = SMILUtil.getComputedStyleSimple(text, "font-size"); + var expectedVal = "75px"; + + // NOTE: There's a very small chance (1/11! = 1/39,916,800) that we'll happen + // to composite our 11 animations in the correct order, in which cast this + // "todo_is" test would sporadically pass. I think this is infrequent enough + // to accept as a sporadic pass rate until this bug is fixed (at which point + // this "todo_is" will become an "is") + todo_is(computedVal, expectedVal, + "deeply-inherited font-size halfway through animation"); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSInvalidValues.xhtml b/dom/smil/test/test_smilCSSInvalidValues.xhtml new file mode 100644 index 000000000..be5da6224 --- /dev/null +++ b/dom/smil/test/test_smilCSSInvalidValues.xhtml @@ -0,0 +1,59 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var invalidTestcaseBundles = [ + new TestcaseBundle(gPropList.opacity, [ + new AnimTestcaseFromTo("", "", { noEffect: true }), + new AnimTestcaseFromTo("", "0.5", { noEffect: true }), + new AnimTestcaseFromTo(".", "0.5", { noEffect: true }), + new AnimTestcaseFromTo("0.5", "-", { noEffect: true }), + new AnimTestcaseFromTo("0.5", "bogus", { noEffect: true }), + new AnimTestcaseFromTo("bogus", "bogus", { noEffect: true }), + ]), + new TestcaseBundle(gPropList.color, [ + new AnimTestcaseFromTo("", "", { noEffect: true }), + new AnimTestcaseFromTo("", "red", { noEffect: true }), + new AnimTestcaseFromTo("greeeen", "red", { noEffect: true }), + new AnimTestcaseFromTo("rgb(red, 255, 255)", "red", { noEffect: true }), + new AnimTestcaseFromTo("#FFFFFFF", "red", { noEffect: true }), + new AnimTestcaseFromTo("bogus", "bogus", { noEffect: true }), + ]), +]; +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // Run the tests + testBundleList(invalidTestcaseBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSPaced.xhtml b/dom/smil/test/test_smilCSSPaced.xhtml new file mode 100644 index 000000000..21040dc70 --- /dev/null +++ b/dom/smil/test/test_smilCSSPaced.xhtml @@ -0,0 +1,44 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSPaced.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <text x="20" y="20">testing 123</text> + <marker/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testBundleList(gPacedBundles, new SMILTimingData(1.0, 6.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilChangeAfterFrozen.xhtml b/dom/smil/test/test_smilChangeAfterFrozen.xhtml new file mode 100644 index 000000000..91e87bc34 --- /dev/null +++ b/dom/smil/test/test_smilChangeAfterFrozen.xhtml @@ -0,0 +1,571 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL when things change after an animation is frozen</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=533291">Mozilla Bug 533291</a> +<p id="display"></p> +<!-- Bug 628848: The following should be display: none but we currently don't + handle percentage lengths properly when the whole fragment is display: none + --> +<div id="content" style="visibility: hidden"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <g id="circleParent"> + <circle cx="0" cy="20" r="15" fill="blue" id="circle"/> + </g> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL values that are context-sensitive **/ + +/* See bugs 533291 and 562815. + + The format of each test is basically: + 1) create some animated and frozen state + 2) test the animated values + 3) change the context + 4) test that context-sensitive animation values have changed + + Ideally, after changing the context (3), the animated state would instantly + update. However, this is not currently the case for many situations. + + For CSS properties we have bug 545282 - In animations involving 'inherit' + / 'currentColor', changes to inherited value / 'color' don't show up in + animated value immediately + + For SVG lengths we have bug 508206 - Relative units used in + animation don't update immediately + + (There are a few of todo_is's in the following tests so that if those bugs + are ever resolved we'll know to update this test case accordingly.) + + So in between (3) and (4) we force a sample. This is currently done by + calling SVGSVGElement.setCurrentTime with the same current time which has the + side effect of forcing a sample. + + What we *are* testing is that we're not too zealous with caching animation + values whilst in the frozen state. Normally we'd say, "Hey, we're frozen, + let's just use the same animation result as last time" but for some + context-sensitive animation values that doesn't work. +*/ + +/* Global Variables */ +const SVGNS = "http://www.w3.org/2000/svg"; + +// Animation parameters -- not used for <set> animation +const ANIM_DUR = "4s"; +const TIME_ANIM_END = "4"; +const TIME_AFTER_ANIM_END = "5"; + +const gSvg = document.getElementById("svg"); +const gCircle = document.getElementById("circle"); +const gCircleParent = document.getElementById("circleParent"); + +SimpleTest.waitForExplicitFinish(); + +// MAIN FUNCTION +// ------------- + +function main() +{ + ok(gSvg.animationsPaused(), "should be paused by <svg> load handler"); + is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + const tests = + [ testBaseValueChange, + testCurrentColorChange, + testCurrentColorChangeUsingStyle, + testCurrentColorChangeOnFallback, + testInheritChange, + testInheritChangeUsingStyle, + testEmUnitChangeOnProp, + testEmUnitChangeOnPropBase, + testEmUnitChangeOnLength, + testPercentUnitChangeOnProp, + testPercentUnitChangeOnLength, + testRelativeFontSize, + testRelativeFontWeight, + testRelativeFont, + testCalcFontSize, + testDashArray, + testClip + ]; + + while (tests.length) { + tests.shift()(); + } + SimpleTest.finish(); +} + +// HELPER FUNCTIONS +// ---------------- +function createAnimSetTo(attrName, toVal) +{ + var anim = document.createElementNS(SVGNS,"set"); + anim.setAttribute("attributeName", attrName); + anim.setAttribute("to", toVal); + return gCircle.appendChild(anim); +} + +function createAnimBy(attrName, byVal) +{ + var anim = document.createElementNS(SVGNS,"animate"); + anim.setAttribute("attributeName", attrName); + anim.setAttribute("dur", ANIM_DUR); + anim.setAttribute("begin","0s"); + anim.setAttribute("by", byVal); + anim.setAttribute("fill", "freeze"); + return gCircle.appendChild(anim); +} + +function createAnimFromTo(attrName, fromVal, toVal) +{ + var anim = document.createElementNS(SVGNS,"animate"); + anim.setAttribute("attributeName", attrName); + anim.setAttribute("dur", ANIM_DUR); + anim.setAttribute("begin","0s"); + anim.setAttribute("from", fromVal); + anim.setAttribute("to", toVal); + anim.setAttribute("fill", "freeze"); + return gCircle.appendChild(anim); +} + +// Common setup code for each test function: seek to 0, and make sure +// the previous test cleaned up its animations. +function setupTest() { + gSvg.setCurrentTime(0); + if (gCircle.firstChild) { + ok(false, "Previous test didn't clean up after itself."); + } +} + +// THE TESTS +// --------- + +function testBaseValueChange() +{ + setupTest(); + var anim = createAnimBy("cx", "50"); + gSvg.setCurrentTime(TIME_ANIM_END); + is(gCircle.cx.animVal.value, 50, + "Checking animated cx as anim ends"); + + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(gCircle.cx.animVal.value, 50, + "Checking animated cx after anim ends"); + + gCircle.setAttribute("cx", 20); + is(gCircle.cx.animVal.value, 70, + "Checking animated cx after anim ends & after changing base val"); + + anim.parentNode.removeChild(anim); // clean up +} + +function testCurrentColorChange() +{ + gCircle.setAttribute("color", "red"); // At first: currentColor=red + var anim = createAnimSetTo("fill", "currentColor"); + + gSvg.setCurrentTime(0); // trigger synchronous sample + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)", + "Checking animated fill=currentColor after animating"); + + gCircle.setAttribute("color", "lime"); // Change: currentColor=lime + // Bug 545282: We should really detect this change and update immediately but + // currently we don't until we get sampled again + todo_is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=currentColor after updating context but before " + + "sampling"); + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=currentColor after updating context"); + + // Clean up + gCircle.removeAttribute("color"); + gCircle.removeChild(gCircle.firstChild); +} + +function testCurrentColorChangeUsingStyle() +{ + setupTest(); + gCircle.setAttribute("style", "color: red"); // At first: currentColor=red + var anim = createAnimSetTo("fill", "currentColor"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)", + "Checking animated fill=currentColor after animating (using style attr)"); + + gCircle.setAttribute("style", "color: lime"); // Change: currentColor=lime + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=currentColor after updating context " + + "(using style attr)"); + + // Clean up + gCircle.removeAttribute("style"); + gCircle.removeChild(gCircle.firstChild); +} + +function getFallbackColor(pServerStr) +{ + return pServerStr.substr(pServerStr.indexOf(" ")+1); +} + +function testCurrentColorChangeOnFallback() +{ + setupTest(); + gCircle.setAttribute("color", "red"); // At first: currentColor=red + var anim = createAnimSetTo("fill", "url(#missingGrad) currentColor"); + + gSvg.setCurrentTime(0); + var fallback = + getFallbackColor(SMILUtil.getComputedStyleSimple(gCircle, "fill")); + is(fallback, "rgb(255, 0, 0)", + "Checking animated fallback fill=currentColor after animating"); + + gCircle.setAttribute("color", "lime"); // Change: currentColor=lime + gSvg.setCurrentTime(0); + fallback = getFallbackColor(SMILUtil.getComputedStyleSimple(gCircle, "fill")); + is(fallback, "rgb(0, 255, 0)", + "Checking animated fallback fill=currentColor after updating context"); + + gCircle.removeAttribute("style"); + gCircle.removeChild(gCircle.firstChild); +} + +function testInheritChange() +{ + setupTest(); + gCircleParent.setAttribute("fill", "red"); // At first: inherit=red + var anim = createAnimSetTo("fill", "inherit"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)", + "Checking animated fill=inherit after animating"); + + gCircleParent.setAttribute("fill", "lime"); // Change: inherit=lime + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=inherit after updating context"); + + gCircleParent.removeAttribute("fill"); + gCircle.removeChild(gCircle.firstChild); +} + +function testInheritChangeUsingStyle() +{ + setupTest(); + gCircleParent.setAttribute("style", "fill: red"); // At first: inherit=red + var anim = createAnimSetTo("fill", "inherit"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)", + "Checking animated fill=inherit after animating (using style attr)"); + + gCircleParent.setAttribute("style", "fill: lime"); // Change: inherit=lime + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=inherit after updating context " + + "(using style attr)"); + + gCircleParent.removeAttribute("style"); + gCircle.removeChild(gCircle.firstChild); +} + +function testEmUnitChangeOnProp() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("font-size", "2em"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "20px", + "Checking animated font-size=2em after animating ends"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "40px", + "Checking animated font-size=2em after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.removeChild(gCircle.firstChild); +} + +function testEmUnitChangeOnPropBase() +{ + // Test the case where the base value for our animation sandwich is + // context-sensitive. + // Currently, this is taken care of by the compositor which keeps a cached + // base value and compares it with the current base value. This test then just + // serves as a regression test in case the compositor's behaviour changes. + setupTest(); + gSvg.setAttribute("font-size", "10px"); // At first: font-size: 10px + gCircleParent.setAttribute("font-size", "1em"); // Base: 10px + var anim = createAnimBy("font-size", "10px"); + + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "20px", + "Checking animated font-size=20px after anim ends"); + + gSvg.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "30px", + "Checking animated font-size=30px after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.removeChild(gCircle.firstChild); +} + +function testEmUnitChangeOnLength() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("cx", "2em"); + + gSvg.setCurrentTime(0); + is(gCircle.cx.animVal.value, 20, + "Checking animated length=2em after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + // Bug 508206: We should really detect this change and update immediately but + // currently we don't until we get sampled again + todo_is(gCircle.cx.animVal.value, 40, + "Checking animated length=2em after updating context but before sampling"); + + gSvg.setCurrentTime(0); + is(gCircle.cx.animVal.value, 40, + "Checking animated length=2em after updating context and after " + + "resampling"); + + gCircleParent.removeAttribute("font-size"); + gCircle.removeChild(gCircle.firstChild); +} + +function testPercentUnitChangeOnProp() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("font-size", "150%"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "15px", + "Checking animated font-size=150% after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "30px", + "Checking animated font-size=150% after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.removeChild(gCircle.firstChild); +} + +function testPercentUnitChangeOnLength() +{ + setupTest(); + var oldHeight = gSvg.getAttribute("height"); + gSvg.setAttribute("height", "100px"); // At first: viewport height: 100px + var anim = createAnimSetTo("cy", "100%"); + + gSvg.setCurrentTime(0); // Force synchronous sample so animation takes effect + // Due to bug 627594 (SVGLength.value for percent value lengths doesn't + // reflect updated viewport until reflow) the following will fail. + // Check that it does indeed fail so that when that bug is fixed this test + // can be updated. + todo_is(gCircle.cy.animVal.value, 100, + "Checking animated length=100% after animating but before reflow"); + // force a layout flush (Bug 627594) + gSvg.getCTM(); + // Even after doing a reflow though we'll still fail due to bug 508206 + // (Relative units used in animation don't update immediately) + todo_is(gCircle.cy.animVal.value, 100, + "Checking animated length=100% after animating but before resampling"); + gSvg.setCurrentTime(0); + // Now we should be up to date + is(gCircle.cy.animVal.value, 100, + "Checking animated length=100% after animating"); + + gSvg.setAttribute("height", "50px"); // Change: height: 50px + // force a layout flush (Bug 627594) + gSvg.getCTM(); + gSvg.setCurrentTime(0); // Bug 508206 + is(gCircle.cy.animVal.value, 50, + "Checking animated length=100% after updating context"); + + gSvg.setAttribute("height", oldHeight); + gCircle.removeChild(gCircle.firstChild); +} + +function testRelativeFontSize() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("font-size", "larger"); + + gSvg.setCurrentTime(0); + var fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + // CSS 2 suggests a scaling factor of 1.2 so we should be looking at something + // around about 12 or so + ok(fsize > 10 && fsize < 20, + "Checking animated font-size > 10px after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + ok(fsize > 20, "Checking animated font-size > 20px after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.removeChild(gCircle.firstChild); +} + +function testRelativeFontWeight() +{ + setupTest(); + gCircleParent.setAttribute("font-weight", "100"); // At first: font-weight 100 + var anim = createAnimSetTo("font-weight", "bolder"); + // CSS 2: 'bolder': Specifies the next weight that is assigned to a font + // that is darker than the inherited one. If there is no such weight, it + // simply results in the next darker numerical value (and the font remains + // unchanged), unless the inherited value was '900', in which case the + // resulting weight is also '900'. + + gSvg.setCurrentTime(0); + var weight = + parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-weight")); + ok(weight > 100, "Checking animated font-weight > 100 after animating"); + + gCircleParent.setAttribute("font-weight", "800"); // Change: font-weight 800 + gSvg.setCurrentTime(0); + weight = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-weight")); + is(weight, 900, + "Checking animated font-weight = 900 after updating context"); + + gCircleParent.removeAttribute("font-weight"); + gCircle.removeChild(gCircle.firstChild); +} + +function testRelativeFont() +{ + // Test a relative font-size as part of a 'font' spec since the code path + // is different in this case + // It turns out that, due to the way we store shorthand font properties, we + // don't need to worry about marking such values as context-sensitive since we + // seem to store them in their relative form. If, however, we change the way + // we store shorthand font properties in the future, this will serve as + // a useful regression test. + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + // We must be sure to set every part of the shorthand property to some + // non-context sensitive value because we want to test that even if only the + // font-size is relative we will update it appropriately. + var anim = + createAnimSetTo("font", "normal normal bold larger/normal sans-serif"); + + gSvg.setCurrentTime(0); + var fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + ok(fsize > 10 && fsize < 20, + "Checking size of shorthand 'font' > 10px after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + ok(fsize > 20, + "Checking size of shorthand 'font' > 20px after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.removeChild(gCircle.firstChild); +} + +function testCalcFontSize() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("font-size", "-moz-calc(110% + 0.1em)"); + + gSvg.setCurrentTime(0); + var fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + // Font size should be 1.1 * 10px + 0.1 * 10px = 12 + is(fsize, 12, "Checking animated calc font-size == 12px after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + is(fsize, 24, "Checking animated calc font-size == 24px after updating " + + "context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.removeChild(gCircle.firstChild); +} + +function testDashArray() +{ + // stroke dasharrays don't currently convert units--but if someone ever fixes + // that, hopefully this test will fail and remind us not to cache percentage + // values in that case + setupTest(); + var oldHeight = gSvg.getAttribute("height"); + var oldWidth = gSvg.getAttribute("width"); + gSvg.setAttribute("height", "100px"); // At first: viewport: 100x100px + gSvg.setAttribute("width", "100px"); + var anim = createAnimFromTo("stroke-dasharray", "0 5", "0 50%"); + + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + + // Now we should be up to date + is(SMILUtil.getComputedStyleSimple(gCircle, "stroke-dasharray"), "0, 50%", + "Checking animated stroke-dasharray after animating"); + + gSvg.setAttribute("height", "50px"); // Change viewport: 50x50px + gSvg.setAttribute("width", "50px"); + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(gCircle, "stroke-dasharray"), "0, 50%", + "Checking animated stroke-dasharray after updating context"); + + gSvg.setAttribute("height", oldHeight); + gSvg.setAttribute("width", oldWidth); + gCircle.removeChild(gCircle.firstChild); +} + +function testClip() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "20px"); // At first: font-size: 20px + + // The clip property only applies to elements that establish a new + // viewport so we need to create a nested svg and add animation to that + var nestedSVG = document.createElementNS(SVGNS, "svg"); + nestedSVG.setAttribute("clip", "rect(0px 0px 0px 0px)"); + gCircleParent.appendChild(nestedSVG); + + var anim = createAnimSetTo("clip", "rect(1em 1em 1em 1em)"); + // createAnimSetTo will make the animation a child of gCircle so we need to + // move it so it targets nestedSVG instead + nestedSVG.appendChild(anim); + + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(nestedSVG, "clip"), + "rect(20px, 20px, 20px, 20px)", + "Checking animated clip rect after animating"); + + gCircleParent.setAttribute("font-size", "10px"); // Change: font-size: 10px + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(nestedSVG, "clip"), + "rect(10px, 10px, 10px, 10px)", + "Checking animated clip rect after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircleParent.removeChild(nestedSVG); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilConditionalProcessing.html b/dom/smil/test/test_smilConditionalProcessing.html new file mode 100644 index 000000000..21d08adb0 --- /dev/null +++ b/dom/smil/test/test_smilConditionalProcessing.html @@ -0,0 +1,80 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Test conditional processing tests applied to animations</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle r="50" fill="blue" id="circle"> + <set attributeName="cy" to="100" begin="0s" dur="100s" id="a"/> + <set attributeName="cx" to="100" begin="a.end" dur="100s" id="b"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +var svg = document.getElementById("svg"), + a = document.getElementById("a"), + b = document.getElementById("b"), + circle = document.getElementById("circle"); + +// Check initial state +svg.setCurrentTime(50); +is(a.getStartTime(), 0, "a has resolved start time at start"); +is(circle.cy.animVal.value, 100, "a is in effect at start"); +is(b.getStartTime(), 100, "b has resolved start time at start"); + +// Add a failing conditional processing test +a.setAttribute("systemLanguage", "no-such-language"); +ok(hasUnresolvedStartTime(a), + "a has unresolved start time with failing conditional processing test"); +is(circle.cy.animVal.value, 0, + "a is not in effect with failing conditional processing test"); +ok(hasUnresolvedStartTime(b), + "b has unresolved start time with failing conditional processing test on a"); + +// Remove failing conditional processing test +a.removeAttribute("systemLanguage"); +is(a.getStartTime(), 0, "a has resolved start time after removing test"); +is(circle.cy.animVal.value, 100, "a is in effect after removing test"); +is(b.getStartTime(), 100, "b has resolved start time after removing test on a"); + +// Add another failing conditional processing test +// According to the spec, if a null string or empty string value is set for +// the 'systemLanguage' attribute, the attribute returns "false". +a.setAttribute("systemLanguage", ""); + +// Fast forward until |a| would have finished +var endEventsReceived = 0; +a.addEventListener("endEvent", function() { endEventsReceived++; }); +svg.setCurrentTime(150); +is(endEventsReceived, 0, + "a does not dispatch end events with failing condition processing test"); +is(circle.cx.animVal.value, 0, + "b is not in effect with failing conditional processing test on a"); + +// Make test pass +a.setAttribute("systemLanguage", "en"); +is(circle.cx.animVal.value, 100, + "b is in effect with passing conditional processing test on a"); + +function hasUnresolvedStartTime(anim) { + // getStartTime throws INVALID_STATE_ERR when there is no current interval + try { + anim.getStartTime(); + return false; + } catch(e) { + return true; + } +} + +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilContainerBinding.xhtml b/dom/smil/test/test_smilContainerBinding.xhtml new file mode 100644 index 000000000..1a47703bf --- /dev/null +++ b/dom/smil/test/test_smilContainerBinding.xhtml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for adding and removing animations from a time container</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-20" cy="20" r="15" fill="blue" id="circle"> + <set attributeName="cy" to="120" begin="0s; 2s" dur="1s" id="b"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for adding and removing animations from a time container **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = getElement("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // Create animation and check initial state + var anim = createAnim(); + anim.setAttribute('begin','b.begin+2s; 6s'); + ok(noStart(anim), "Animation has start time before attaching to document."); + + // Attach animation to container + var circle = getElement("circle"); + circle.appendChild(anim); + + // Check state after attaching + is(anim.getStartTime(), 2); + + // Unbind from tree -- the syncbase instance time(s) should become unresolved + // but the offset time should remain + anim.parentNode.removeChild(anim); + is(anim.getStartTime(), 6); + + // Rebind and check everything is re-resolved + circle.appendChild(anim); + is(anim.getStartTime(), 2); + + // Advance document time to t=1s + // Now the current interval for b is 2s-3s but the current interval for anim + // is still 2s-2.5s based on b's previous interval + svg.setCurrentTime(1); + is(anim.getStartTime(), 2); + + // Unbind + anim.parentNode.removeChild(anim); + is(anim.getStartTime(), 6); + + // Rebind + // At this point only the current interval will be re-added to anim (this is + // for consistency since old intervals may or may not have been filtered). + // Therefore the start time should be 4s instead of 2s. + circle.appendChild(anim); + is(anim.getStartTime(), 4); + + SimpleTest.finish(); +} + +function createAnim() { + const svgns="http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns,'set'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('to','100'); + anim.setAttribute('dur','0.5s'); + return anim; +} + +function noStart(elem) { + var exceptionCaught = false; + + try { + elem.getStartTime(); + } catch(e) { + exceptionCaught = true; + is (e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is (e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime."); + } + + return exceptionCaught; +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCrossContainer.xhtml b/dom/smil/test/test_smilCrossContainer.xhtml new file mode 100644 index 000000000..2067973d6 --- /dev/null +++ b/dom/smil/test/test_smilCrossContainer.xhtml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for moving animations between time containers</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svga" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-20" cy="20" r="15" fill="blue" id="circlea"/> +</svg> +<svg id="svgb" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-20" cy="20" r="15" fill="blue" id="circleb"> + <set attributeName="cy" to="120" begin="4s" dur="1s" id="syncb"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for moving animations between time containers **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svga = getElement("svga"); + ok(svga.animationsPaused(), "should be paused by <svg> load handler"); + is(svga.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + svga.setCurrentTime(1); + + var svgb = getElement("svgb"); + ok(svgb.animationsPaused(), "should be paused by <svg> load handler"); + is(svgb.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + svgb.setCurrentTime(1); + + // Create animation and check initial state + var anim = createAnim(); + ok(noStart(anim), "Animation has start time before attaching to document"); + + // Attach animation to first container + var circlea = getElement("circlea"); + var circleb = getElement("circleb"); + circlea.appendChild(anim); + + // Check state after attaching + is(anim.getStartTime(), 2, + "Unexpected start time after attaching animation to target"); + is(circlea.cx.animVal.value, -20, + "Unexpected animated value for yet-to-start animation"); + is(circleb.cx.animVal.value, -20, + "Unexpected animated value for unanimated target"); + + // Move animation from first container to second + circleb.appendChild(anim); + + // Advance first container and check animation has no effect + svga.setCurrentTime(2); + is(anim.getStartTime(), 2, + "Unexpected start time after moving animation"); + is(circlea.cx.animVal.value, -20, + "Unexpected animated value for non-longer-animated target"); + is(circleb.cx.animVal.value, -20, + "Unexpected animated value for now yet-to-start animation"); + + // Advance second container and check the animation only affects it + svgb.setCurrentTime(2); + is(anim.getStartTime(), 2, "Start time changed after time container seek"); + is(circlea.cx.animVal.value, -20, + "Unanimated target changed after seek on other container"); + is(circleb.cx.animVal.value, 100, "Animated target not animated after seek"); + + // Remove animation so that it belongs to no container and check that + // advancing the second container to the next milestone doesn't cause a crash + // (when the animation controller goes to run the next milestone sample). + anim.parentNode.removeChild(anim); + svgb.setCurrentTime(3); + + // Do likewise with syncbase relationships + + // Create the syncbase relationship + anim.setAttribute('begin', 'syncb.begin'); + + // Attach to second time container (where t=3s) + circleb.appendChild(anim); + is(anim.getStartTime(), 4, + "Unexpected start time for cross-time container syncbase dependency"); + + // Move to first time container (where t=1s). + // Because we're dealing with different time containers and both are paused, + // future times are effectively unresolved. + circlea.appendChild(anim); + ok(noStart(anim), "Unexpected start time for paused time container"); + + SimpleTest.finish(); +} + +function createAnim() { + const svgns="http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns,'set'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('to','100'); + anim.setAttribute('begin','2s'); + anim.setAttribute('dur','1s'); + return anim; +} + +function noStart(elem) { + var exceptionCaught = false; + + try { + elem.getStartTime(); + } catch(e) { + exceptionCaught = true; + is (e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is (e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime"); + } + + return exceptionCaught; +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml b/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml new file mode 100644 index 000000000..b2af10c6f --- /dev/null +++ b/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml @@ -0,0 +1,103 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=699143 +--> +<head> + <title>Test for Bug 699143</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=699143">Mozilla Bug 699143</a> +<p id="display"></p> +<div id="content" style="display: none"> + <svg xmlns="http://www.w3.org/2000/svg"> + <rect id="r" height="500px" width="500px" fill="blue"/> + </svg> +</div> +<pre id="test"> +<script type="text/javascript"> +<![CDATA[ + +/** Test for Bug 699143 **/ +SimpleTest.waitForExplicitFinish(); + +// Values for 'width' attr on the <rect> above +const INITIAL_VAL = "500px" +const FROM_VAL = "20px"; +const TO_VAL = "80px"; + +// Helper functions + +// This function allows 10ms to pass +function allowTimeToPass() { + var initialDate = new Date(); + while (new Date() - initialDate < 10) {} +} + +// This function returns a newly created <animate> element for use in this test +function createAnim() { + var a = document.createElementNS('http://www.w3.org/2000/svg', 'animate'); + a.setAttribute('attributeName', 'width'); + a.setAttribute('from', FROM_VAL); + a.setAttribute('to', TO_VAL); + a.setAttribute('begin', 'indefinite'); + a.setAttribute('dur', '3s'); + a.setAttribute('fill', 'freeze'); + return a; +} + +// Main Functions +function main() { + // In unpatched Firefox builds, we'll only trigger Bug 699143 if we insert + // an animation and call beginElement() **after** the document start-time. + // Hence, we use executeSoon here to allow some time to pass. (And then + // we'll use a short busy-loop, for good measure.) + SimpleTest.executeSoon(runTest); +} + +function runTest() { + var svg = SMILUtil.getSVGRoot(); + + // In case our executeSoon fired immediately, we force a very small amount + // of time to pass here, using a 10ms busy-loop. + allowTimeToPass(); + + is(svg.getCurrentTime(), 0, + "even though we've allowed time to pass, we shouldn't have bothered " + + "updating the current time, since there aren't any animation elements"); + + // Insert an animation elem (should affect currentTime but not targeted attr) + var r = document.getElementById("r"); + var a = createAnim(); + r.appendChild(a); + isnot(svg.getCurrentTime(), 0, + "insertion of first animation element should have triggered a " + + "synchronous sample and updated our current time"); + is(r.width.animVal.valueAsString, INITIAL_VAL, + "inserted animation shouldn't have affected its targeted attribute, " + + "since it doesn't have any intervals yet"); + + // Trigger the animation & be sure it takes effect + a.beginElement(); + is(r.width.animVal.valueAsString, FROM_VAL, + "beginElement() should activate our animation & set its 'from' val"); + + // Rewind to time=0 & check target attr, to be sure beginElement()-generated + // interval starts later than that. + svg.setCurrentTime(0); + is(r.width.animVal.valueAsString, INITIAL_VAL, + "after rewinding to 0, our beginElement()-generated interval " + + "shouldn't be active yet"); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); + +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilExtDoc.xhtml b/dom/smil/test/test_smilExtDoc.xhtml new file mode 100644 index 000000000..772aebdaa --- /dev/null +++ b/dom/smil/test/test_smilExtDoc.xhtml @@ -0,0 +1,80 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=628888 +--> +<head> + <title>Test for Bug 628888 - Animations in external document sometimes don't run</title> + <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body style="margin:0px"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=628888">Mozilla Bug 628888</a> +<p id="display"></p> +<div id="content" style="background: red; width: 50px; height: 50px"/> + +<pre id="test"> +<script type="application/javascript"> +<![CDATA[ + +/* Test for Bug 628888 - Animations in external document sometimes don't run + * + * This bug concerns a condition where an external document is loaded after the + * page show event is dispatched, leaving the external document paused. + * + * To reproduce the bug we attach an external document with animation after the + * page show event has fired. + * + * However, it is difficult to test if the animation is playing or not since we + * don't receive events from animations running in an external document. + * + * Our approach is to simply render the result to a canvas (which requires + * elevated privileges and that is why we are using a MochiTest rather + * than a reftest) and poll one of the pixels to see if it changes colour. + * + * This should mean the test succeeds quickly but fails slowly. + */ + +const POLL_INTERVAL = 100; // ms +const POLL_TIMEOUT = 10000; // ms +var accumulatedWaitTime = 0; + +function pageShow() +{ + var content = document.getElementById("content"); + content.style.filter = "url(smilExtDoc_helper.svg#filter)"; + window.setTimeout(checkResult, 0); +} + +function checkResult() +{ + var content = document.getElementById("content"); + var bbox = content.getBoundingClientRect(); + + var canvas = SpecialPowers.snapshotRect(window, bbox); + var ctx = canvas.getContext("2d"); + + var imgd = ctx.getImageData(bbox.width/2, bbox.height/2, 1, 1); + var isGreen = (imgd.data[0] == 0) && + (imgd.data[1] == 255) && + (imgd.data[2] == 0); + if (isGreen) { + ok(true, "Filter is animated as expected"); + } else if (accumulatedWaitTime >= POLL_TIMEOUT) { + ok(false, "No animation detected after waiting " + POLL_TIMEOUT + "ms"); + } else { + accumulatedWaitTime += POLL_INTERVAL; + window.setTimeout(checkResult, POLL_INTERVAL); + return; + } + // Hide our content since mochitests normally try to be visually "quiet" + content.style.display = 'none'; + SimpleTest.finish(); +} +window.addEventListener('pageshow', pageShow, false); +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilFillMode.xhtml b/dom/smil/test/test_smilFillMode.xhtml new file mode 100644 index 000000000..b0f4b84c7 --- /dev/null +++ b/dom/smil/test/test_smilFillMode.xhtml @@ -0,0 +1,86 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL fill modes</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL fill modes **/ + +/* Global Variables */ +const svgns="http://www.w3.org/2000/svg"; +var svg = document.getElementById("svg"); +var circle = document.getElementById('circle'); + +SimpleTest.waitForExplicitFinish(); + +function createAnim() { + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('dur','4s'); + anim.setAttribute('begin','0s'); + anim.setAttribute('values', '10; 20'); + return circle.appendChild(anim); +} + +function main() { + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var tests = + [ testSetLaterA, + testSetLaterB, + testRemoveLater ]; + for (var i = 0; i < tests.length; i++) { + var anim = createAnim(); + svg.setCurrentTime(0); + tests[i](anim); + anim.parentNode.removeChild(anim); + } + SimpleTest.finish(); +} + +function checkSample(time, expectedValue) { + svg.setCurrentTime(time); + is(circle.cx.animVal.value, expectedValue, + "Updated fill mode not applied to animation"); +} + +// Test that we can update the fill mode after an interval has played and it +// will be updated correctly. +function testSetLaterA(anim) { + checkSample(5, -100); + anim.setAttribute('fill', 'freeze'); + is(circle.cx.animVal.value, 20, + "Fill not applied for retrospectively set fill mode"); +} + +function testSetLaterB(anim) { + anim.setAttribute('fill', 'freeze'); + checkSample(5, 20); +} + +function testRemoveLater(anim) { + anim.setAttribute('fill', 'freeze'); + checkSample(5, 20); + anim.setAttribute('fill', 'remove'); + is(circle.cx.animVal.value, -100, + "Fill not removed for retrospectively set fill mode"); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilGetSimpleDuration.xhtml b/dom/smil/test/test_smilGetSimpleDuration.xhtml new file mode 100644 index 000000000..5c4dc33eb --- /dev/null +++ b/dom/smil/test/test_smilGetSimpleDuration.xhtml @@ -0,0 +1,86 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for getSimpleDuration Behavior </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" + from="20" to="100" begin="1s" id="anim"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for getSimpleDuration Behavior **/ + +/* Global Variables */ +var svg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); + +function main() { + var anim = document.getElementById("anim"); + + /* Check initial state */ + checkForException(anim, "dur not set"); + + /* Check basic operation */ + anim.setAttribute("dur", "1s"); + is(anim.getSimpleDuration(), 1); + anim.setAttribute("dur", "1.5s"); + is(anim.getSimpleDuration(), 1.5); + + /* Check exceptional states */ + anim.setAttribute("dur", "0s"); + checkForException(anim, "dur=0s"); + anim.setAttribute("dur", "-1s"); + checkForException(anim, "dur=-1s"); + anim.setAttribute("dur", "indefinite"); + checkForException(anim, "dur=indefinite"); + anim.setAttribute("dur", "media"); + checkForException(anim, "dur=media"); + anim.setAttribute("dur", "abc"); + checkForException(anim, "dur=abc"); + anim.removeAttribute("dur"); + checkForException(anim, "dur not set"); + + /* Check range/syntax */ + anim.setAttribute("dur", "100ms"); + millisecondCompare(anim.getSimpleDuration(), 0.1); + anim.setAttribute("dur", "24h"); + is(anim.getSimpleDuration(), 60 * 60 * 24); + + SimpleTest.finish(); +} + +function millisecondCompare(a, b) { + is(Math.round(a * 1000), Math.round(b * 1000)); +} + +function checkForException(anim, descr) { + var gotException = false; + try { + var dur = anim.getSimpleDuration(); + } catch(e) { + is (e.name, "NotSupportedError", + "Wrong exception from getSimpleDuration"); + is (e.code, DOMException.NOT_SUPPORTED_ERR, + "Wrong exception from getSimpleDuration"); + gotException = true; + } + ok(gotException, + "Exception not thrown for indefinite simple duration when " + descr); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilGetStartTime.xhtml b/dom/smil/test/test_smilGetStartTime.xhtml new file mode 100644 index 000000000..9b608487c --- /dev/null +++ b/dom/smil/test/test_smilGetStartTime.xhtml @@ -0,0 +1,103 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for getStartTime Behavior </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" + from="20" to="100" begin="indefinite" dur="1s" id="anim"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for getStartTime Behavior **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = document.getElementById("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var anim = document.getElementById("anim"); + // indefinite + var exceptionCaught = false; + try { + anim.getStartTime(); + } catch(e) { + exceptionCaught = true; + is(e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is(e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime."); + } + ok(exceptionCaught, "No exception thrown for indefinite start time."); + + // 1s + anim.setAttribute("begin", "1s"); + is(anim.getStartTime(), 1, "Unexpected start time with begin=1s"); + + // We have to be careful here when choosing a negative time that we choose + // a time that will create an interval that reaches past t=0 as SMIL has + // special rules for throwing away intervals that end before t=0 + anim.setAttribute("begin", "-0.5s"); + is(anim.getStartTime(), -0.5, "Unexpected start time with begin=-0.5s"); + + // Once the animation has begun, the begin time is fixed so we need to end the + // element (or advance the timeline) to override the previous start time + anim.endElement(); + + // However, now we have an end instance, and the SMIL model dictates that if + // we have end instances and no end event conditions and all end instances are + // before our next begin, there's no valid interval. To overcome this we add + // an indefinite end. + anim.setAttribute("end", "indefinite"); + + // Now test over the lifetime of the animation when there are multiple + // intervals + anim.setAttribute("begin", "1s; 3s"); + is(anim.getStartTime(), 1, "Unexpected start time before first interval"); + + svg.setCurrentTime(1); + is(anim.getStartTime(), 1, + "Unexpected start time at start of first interval"); + + svg.setCurrentTime(1.5); + is(anim.getStartTime(), 1, "Unexpected start time during first interval"); + + svg.setCurrentTime(2); + is(anim.getStartTime(), 3, "Unexpected start time after first interval"); + + svg.setCurrentTime(3); + is(anim.getStartTime(), 3, "Unexpected start time during second interval"); + + svg.setCurrentTime(4); + exceptionCaught = false; + try { + anim.getStartTime(); + } catch(e) { + exceptionCaught = true; + is(e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is(e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime."); + } + ok(exceptionCaught, "No exception thrown for in postactive state."); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilHyperlinking.xhtml b/dom/smil/test/test_smilHyperlinking.xhtml new file mode 100644 index 000000000..542a02073 --- /dev/null +++ b/dom/smil/test/test_smilHyperlinking.xhtml @@ -0,0 +1,233 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for hyperlinking</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display:none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL keySplines **/ + +/* Global Variables */ +const SVGNS="http://www.w3.org/2000/svg"; +var gSvg = document.getElementById("svg"); +var gAnim; + +var gTestStages = + [ testActive, + testSeekToFirst, + testKickStart, + testKickStartWithUnresolved, + testFiltering + ]; + +SimpleTest.waitForExplicitFinish(); + +function continueTest() +{ + if (gTestStages.length == 0) { + SimpleTest.finish(); + return; + } + + window.location.hash = ""; + if (gAnim) { + gAnim.parentNode.removeChild(gAnim); + } + gAnim = createAnim(); + gSvg.setCurrentTime(0); + gTestStages.shift()(); +} + +function createAnim() { + var anim = document.createElementNS(SVGNS,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('from','0'); + anim.setAttribute('to','100'); + anim.setAttribute('dur','1s'); + anim.setAttribute('begin','indefinite'); + anim.setAttribute('id','anim'); + return document.getElementById('circle').appendChild(anim); +} + +// Traversing a hyperlink, condition 1: +// +// "If the target element is active, seek the document time back to the +// (current) begin time of the element. If there are multiple begin times, use +// the begin time that corresponds to the current "begin instance"." +// +function testActive() { + gAnim.setAttribute('begin','2s; 4s'); + gSvg.setCurrentTime(2.5); + fireLink(rewindActiveInterval1); +} + +function rewindActiveInterval1() { + is(gSvg.getCurrentTime(), 2, + "Unexpected time after activating link to animation in the middle of " + + "first active interval"); + + // Seek to second interval + gSvg.setCurrentTime(4.5); + fireLink(rewindActiveInterval2); +} + +function rewindActiveInterval2() { + is(gSvg.getCurrentTime(), 4, + "Unexpected time after activating link to animation in the middle of " + + "second active interval"); + + // Try a negative time + gAnim.setAttribute("begin", "-0.5"); + gSvg.setCurrentTime(0.2); + fireLink(rewindActiveIntervalAtZero); +} + +function rewindActiveIntervalAtZero() { + is(gSvg.getCurrentTime(), 0, + "Unexpected time after activating link to animation in the middle of " + + "an active interval that overlaps zero"); + + continueTest(); +} + +// Traversing a hyperlink, condition 2: +// +// "Else if the target element begin time is resolved (i.e., there is any +// resolved time in the list of begin times, or if the begin time was forced by +// an earlier hyperlink or a beginElement() method call), seek the document time +// (forward or back, as needed) to the earliest resolved begin time of the +// target element. Note that the begin time may be resolved as a result of an +// earlier hyperlink, DOM or event activation. Once the begin time is resolved, +// hyperlink traversal always seeks." +// +function testSeekToFirst() { + // Seek forwards + gAnim.setAttribute('begin','2s'); + gSvg.setCurrentTime(0); + fireLink(forwardToInterval1); +} + +function forwardToInterval1() { + is(gSvg.getCurrentTime(), 2, + "Unexpected time after activating link to animation scheduled to start " + + "the future"); + + // Seek backwards + gSvg.setCurrentTime(3.5); + fireLink(backwardToInterval1); +} + +function backwardToInterval1() { + is(gSvg.getCurrentTime(), 2, + "Unexpected time after activating link to animation that ran in the past"); + + // What if the first begin instance is negative? + gAnim.setAttribute('begin','-0.5s'); + gSvg.setCurrentTime(1); + fireLink(backwardToZero); +} + +function backwardToZero() { + is(gSvg.getCurrentTime(), 0, + "Unexpected time after activating link to animation that ran in the " + + "past with a negative time"); + + continueTest(); +} + +// Traversing a hyperlink, condition 3: +// +// "Else (animation begin time is unresolved) just resolve the target animation +// begin time at current document time. Disregard the sync-base or event base of +// the animation, and do not "back-propagate" any timing logic to resolve the +// child, but rather treat it as though it were defined with begin="indefinite" +// and just resolve begin time to the current document time." +// +function testKickStart() { + gSvg.setCurrentTime(1); + fireLink(startedAt1s); +} + +function startedAt1s() { + is(gSvg.getCurrentTime(), 1, + "Unexpected time after kick-starting animation with indefinite start " + + "by hyperlink"); + is(gAnim.getStartTime(), 1, + "Unexpected start time for kick-started animation"); + + continueTest(); +} + +function testKickStartWithUnresolved() { + gAnim.setAttribute("begin", "circle.click"); + gSvg.setCurrentTime(3); + fireLink(startedAt3s); +} + +function startedAt3s() { + is(gSvg.getCurrentTime(), 3, + "Unexpected time after kick-starting animation with unresolved start " + + "by hyperlink"); + is(gAnim.getStartTime(), 3, + "Unexpected start time for kick-started animation with unresolved begin " + + "condition"); + + continueTest(); +} + +function testFiltering() { + gAnim.setAttribute('begin','-3s; 1s; 2s; 3s; 4s; 5s; 6s; 7s; 8s; 9s; 10s'); + gSvg.setCurrentTime(12); + fireLink(rewindToFirst); +} + +function rewindToFirst() { + is(gSvg.getCurrentTime(), 1, + "Unexpected time after triggering animation with a hyperlink after " + + "numerous intervals have passed"); + + continueTest(); +} + +function fireLink(callback) { + // First we need to reset the hash because otherwise the redundant hashchange + // events will be suppressed + if (window.location.hash === '') { + fireLinkPart2(callback); + } else { + window.location.hash = ''; + window.addEventListener("hashchange", + function clearHash() { + window.removeEventListener("hashchange", clearHash, false); + window.setTimeout(fireLinkPart2, 0, callback); + }, + false); + } +} + +function fireLinkPart2(callback) { + window.addEventListener("hashchange", + function triggerCallback() { + window.removeEventListener("hashchange", triggerCallback, false); + window.setTimeout(callback, 0); + }, + false); + window.location.hash = '#anim'; +} + +window.addEventListener("load", continueTest, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilInvalidValues.html b/dom/smil/test/test_smilInvalidValues.html new file mode 100644 index 000000000..9cd03f49b --- /dev/null +++ b/dom/smil/test/test_smilInvalidValues.html @@ -0,0 +1,113 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=941315 +--> +<head> + <meta charset="utf-8"> + <title>Test invalid values cause the model to be updated (bug 941315)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=941315">Mozilla Bug 941315</a> +<p id="display"></p> +<div id="content" style="display: none"> +<svg width="100%" height="1" onload="this.pauseAnimations()"> + <rect> + <animate id="a" dur="100s"/> + <animate id="b" dur="5s" begin="a.end"/> + </rect> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + var a = $('a'), + b = $('b'); + + // Animation doesn't start until onload + SimpleTest.waitForExplicitFinish(); + window.addEventListener("load", runTests, false); + + // Make testing getStartTime easier + SVGAnimationElement.prototype.safeGetStartTime = function() { + try { + return this.getStartTime(); + } catch(e) { + if (e.name == "InvalidStateError" && + e.code == DOMException.INVALID_STATE_ERR) { + return 'none'; + } else { + ok(false, "Unexpected exception: " + e); + return null; + } + } + }; + + function runTests() { + [testSimpleDuration, testMin, testMax, testRepeatDur, testRepeatCount] + .forEach(function(test) { + is(b.getStartTime(), 100, "initial state before running " + test.name); + test(); + is(b.getStartTime(), 100, "final state after running " + test.name); + }); + SimpleTest.finish(); + } + + function testSimpleDuration() { + // Verify a valid value updates as expected + a.setAttribute("dur", "50s"); + is(b.safeGetStartTime(), 50, "valid simple duration"); + + // Check an invalid value also causes the model to be updated + a.setAttribute("dur", "abc"); // -> indefinite + is(b.safeGetStartTime(), "none", "invalid simple duration"); + + // Restore state + a.setAttribute("dur", "100s"); + } + + function testMin() { + a.setAttribute("min", "200s"); + is(b.safeGetStartTime(), 200, "valid min duration"); + + a.setAttribute("min", "abc"); // -> indefinite + is(b.safeGetStartTime(), 100, "invalid min duration"); + + a.removeAttribute("min"); + } + + function testMax() { + a.setAttribute("max", "50s"); + is(b.safeGetStartTime(), 50, "valid max duration"); + + a.setAttribute("max", "abc"); // -> indefinite + is(b.safeGetStartTime(), 100, "invalid max duration"); + + a.removeAttribute("max"); + } + + function testRepeatDur() { + a.setAttribute("repeatDur", "200s"); + is(b.safeGetStartTime(), 200, "valid repeatDur duration"); + + a.setAttribute("repeatDur", "abc"); // -> indefinite + is(b.safeGetStartTime(), 100, "invalid repeatDur duration"); + + a.removeAttribute("repeatDur"); + } + + function testRepeatCount() { + a.setAttribute("repeatCount", "2"); + is(b.safeGetStartTime(), 200, "valid repeatCount duration"); + + a.setAttribute("repeatCount", "abc"); // -> indefinite + is(b.safeGetStartTime(), 100, "invalid repeatCount duration"); + + a.removeAttribute("repeatCount"); + } +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilKeySplines.xhtml b/dom/smil/test/test_smilKeySplines.xhtml new file mode 100644 index 000000000..a7ccb58c4 --- /dev/null +++ b/dom/smil/test/test_smilKeySplines.xhtml @@ -0,0 +1,296 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL keySplines</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL keySplines **/ + +/* Global Variables */ +const svgns="http://www.w3.org/2000/svg"; +var svg = document.getElementById("svg"); +var circle = document.getElementById('circle'); + +SimpleTest.waitForExplicitFinish(); + +function createAnim() { + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','0s'); + anim.setAttribute('fill', 'freeze'); + return circle.appendChild(anim); +} + +function main() { + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var tests = + [ testSimpleA, // these first four are from SVG 1.1 + testSimpleB, + testSimpleC, + testSimpleD, + testSimpleE, // bug 501569 + testMultipleIntervalsA, + testMultipleIntervalsB, + testMultipleIntervalsC, + testOneValue, + testFromTo, + testWrongNumSplines, + testToAnimation, + testOkSyntax, + testBadSyntaxA, + testBadSyntaxB + ]; + for (var i = 0; i < tests.length; i++) { + var anim = createAnim(); + svg.setCurrentTime(0); + tests[i](anim); + anim.parentNode.removeChild(anim); + } + SimpleTest.finish(); +} + +function checkSample(time, expectedValue) { + svg.setCurrentTime(time); + is(circle.cx.animVal.value, expectedValue); +} + +function checkSampleRough(time, expectedValue, precision) { + const defaultPrecision = 0.00001; + if (typeof precision == "undefined") { + precision = defaultPrecision; + } + svg.setCurrentTime(time); + var diff = Math.abs(expectedValue - circle.cx.animVal.value); + ok(diff <= precision, + "Unexpected sample value got " + circle.cx.animVal.value + + ", expected " + expectedValue + " [error is " + diff + + ", tolerance is " + precision + "]"); +} + +/* + * These first four tests are the examples given in SVG 1.1, section 19.2.7 + */ + +function testSimpleA(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 0 1 1'); + checkSample(0, 10); + checkSample(1, 12.5); + checkSample(2, 15); + checkSample(3, 17.5); + checkSample(4, 20); +} + +function testSimpleB(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '.5 0 .5 1'); + checkSample(0, 10); + checkSampleRough(1, 11.058925); + checkSample(2, 15); + checkSampleRough(3, 18.941075); + checkSample(4, 20); +} + +function testSimpleC(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 .75 .25 1'); + checkSample(0, 10); + checkSampleRough(1, 18.101832); + checkSampleRough(2, 19.413430); + checkSampleRough(3, 19.886504); + checkSample(4, 20); +} + +function testSimpleD(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '1 0 .25 .25'); + checkSample(0, 10); + checkSampleRough(1, 10.076925); + checkSampleRough(2, 10.644369); + checkSampleRough(3, 16.908699); + checkSample(4, 20); +} + +// Bug 501569 -- nsSMILKeySpline(1, 0, 0, 1) miscalculates values just under 0.5 +function testSimpleE(anim) { + anim.setAttribute('dur','10s'); + anim.setAttribute('values', '0; 10'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '1 0 0 1'); + checkSample(0, 0); + checkSampleRough(0.001, 0); + checkSampleRough(4.95, 3.409174); + checkSampleRough(4.98, 3.819443); + checkSampleRough(4.99, 4.060174); + checkSampleRough(4.999, 4.562510); + checkSample(5, 5); + checkSampleRough(5.001, 5.437490); + checkSampleRough(5.01, 5.939826); + checkSampleRough(5.015, 6.075002); + checkSampleRough(5.02, 6.180557); + checkSampleRough(9.9999, 10); + checkSample(10, 10); +} + +function testMultipleIntervalsA(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20; 30'); + anim.setAttribute('keyTimes', '0; 0.25; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 0 1 1; .5 0 .5 1;'); + checkSample(0.5, 15); + checkSampleRough(0.999, 20, 0.02); + checkSample(1, 20); + checkSampleRough(1.001, 20, 0.05); + checkSample(2.5, 25); + checkSampleRough(3.25, 29, 0.1); +} + +function testMultipleIntervalsB(anim) { + // as above but without keyTimes + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20; 30'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 0 1 1; .5 0 .5 1;'); + checkSample(1, 15); + checkSampleRough(1.999, 20, 0.01); + checkSample(2, 20); + checkSampleRough(2.001, 20, 0.01); + checkSample(3, 25); + checkSampleRough(3.5, 29, 0.1); +} + +function testMultipleIntervalsC(anim) { + // test some unusual (but valid) syntax + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20; 30'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', ' 0 .75 0.25 1 ; 1, 0 ,.25 .25 \t'); + checkSampleRough(3.5, 26.9, 0.2); +} + +function testOneValue(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '5'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 0 1 1'); + checkSample(0, 5); + checkSample(1.5, 5); +} + +function testFromTo(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('from', '10'); + anim.setAttribute('to', '20'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '.5 0 .5 1'); + checkSample(0, 10); + checkSampleRough(1, 11, 0.1); +} + +function testWrongNumSplines(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('from', '10'); + anim.setAttribute('to', '20'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '.5 0 .5 1; 0 0 1 1'); + // animation is in error, should do nothing + checkSample(1.5, -100); +} + +function testToAnimation(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('to', '20'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '.5 0 .5 1'); + checkSample(0, -100); + checkSampleRough(1, -87.3, 0.1); +} + +function testOkSyntax(anim) { + var okStrs = ['0,0,0,0', // all commas + ' 0 0 , 0 ,0 ', // mix of separators + '0 0 0 0;', // trailing semi-colon + '0 0 0 0 ;']; // " " + + for (var i = 0; i < okStrs.length; i++) { + testAnim = createAnim(); + testAnim.setAttribute('dur','4s'); + testAnim.setAttribute('from', '0'); + testAnim.setAttribute('to', '20'); + testAnim.setAttribute('calcMode', 'spline'); + testAnim.setAttribute('keySplines', okStrs[i]); + checkSample(4, 20); + testAnim.parentNode.removeChild(testAnim); + } +} + +function testBadSyntaxA(anim) { + var badStrs = ['', // empty + ' ', // whitespace only + '0,1.1,0,0', // bad range + '0,0,0,-0.1', // " " + ' 0 0 , 0 0 ,', // stray comma + '1-1 0 0', // path-style separators + '0 0 0', // wrong number of values + '0 0 0 0 0', // " " + '0 0 0 0 0 0 0 0', // " " + '0 0 0; 0 0 0 0', // " " + '0 0 0; 0', // mis-placed semi-colon + ';0 0 0 0']; // " " + + for (var i = 0; i < badStrs.length; i++) { + testAnim = createAnim(); + testAnim.setAttribute('dur','4s'); + testAnim.setAttribute('from', '0'); + testAnim.setAttribute('to', '20'); + testAnim.setAttribute('calcMode', 'spline'); + testAnim.setAttribute('keySplines', badStrs[i]); + checkSample(4, -100); + testAnim.parentNode.removeChild(testAnim); + } +} + +function testBadSyntaxB(anim) { + // test some illegal syntax + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20; 30'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', ' 0 .75 0.25 1 ; 1, A0 ,.25 .25 \t'); + // animation is in error, should do nothing + checkSample(3.5, -100); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilKeyTimes.xhtml b/dom/smil/test/test_smilKeyTimes.xhtml new file mode 100644 index 000000000..85266ed19 --- /dev/null +++ b/dom/smil/test/test_smilKeyTimes.xhtml @@ -0,0 +1,391 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL keyTimes</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=557885">Mozilla Bug + 557885</a> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL keyTimes **/ + +var gSvg = document.getElementById("svg"); +SimpleTest.waitForExplicitFinish(); + +function main() +{ + gSvg.pauseAnimations(); + + var testCases = Array(); + + // Simple case + testCases.push({ + 'attr' : { 'values': '0; 50; 100', + 'keyTimes': '0; .8; 1' }, + 'times': [ [ 4, 25 ], + [ 8, 50 ], + [ 9, 75 ], + [ 10, 100 ] ] + }); + + // Parsing tests + testCases.push(parseOk(' 0 ; .8;1 ')); // extra whitespace + testCases.push(parseNotOk(';0; .8; 1')); // leading semi-colon + testCases.push(parseNotOk('; .8; 1')); // leading semi-colon + testCases.push(parseOk('0; .8; 1;')); // trailing semi-colon + testCases.push(parseNotOk('')); // empty string + testCases.push(parseNotOk(' ')); // empty string + testCases.push(parseNotOk('0; .8')); // too few values + testCases.push(parseNotOk('0; .8; .9; 1')); // too many values + testCases.push(parseNotOk('0; 1; .8')); // non-increasing + testCases.push(parseNotOk('0; .8; .9')); // final value non-1 with + // calcMode=linear + testCases.push(parseOk('0; .8; .9', { 'calcMode': 'discrete' })); + testCases.push(parseNotOk('0.01; .8; 1')); // first value not 0 + testCases.push(parseNotOk('0.01; .8; 1', { 'calcMode': 'discrete' })); + // first value not 0 + testCases.push(parseNotOk('0; .8; 1.1')); // out of range + testCases.push(parseNotOk('-0.1; .8; 1')); // out of range + + + // 2 values + testCases.push({ + 'attr' : { 'values': '0; 50', + 'keyTimes': '0; 1' }, + 'times': [ [ 6, 30 ] ] + }); + + // 1 value + testCases.push({ + 'attr' : { 'values': '50', + 'keyTimes': ' 0' }, + 'times': [ [ 7, 50 ] ] + }); + + // 1 bad value + testCases.push({ + 'attr' : { 'values': '50', + 'keyTimes': '0.1' }, + 'times': [ [ 0, -100 ] ] + }); + + // 1 value, calcMode=discrete + testCases.push({ + 'attr' : { 'values': '50', + 'calcMode': 'discrete', + 'keyTimes': ' 0' }, + 'times': [ [ 7, 50 ] ] + }); + + // 1 bad value, calcMode=discrete + testCases.push({ + 'attr' : { 'values': '50', + 'calcMode': 'discrete', + 'keyTimes': '0.1' }, + 'times': [ [ 0, -100 ] ] + }); + + // from-to + testCases.push({ + 'attr' : { 'from': '10', + 'to': '20', + 'keyTimes': '0.0; 1.0' }, + 'times': [ [ 3.5, 13.5 ] ] + }); + + // from-to calcMode=discrete + testCases.push({ + 'attr' : { 'from': '10', + 'to': '20', + 'calcMode': 'discrete', + 'keyTimes': '0.0; 0.7' }, + 'times': [ [ 0, 10 ], + [ 6.9, 10 ], + [ 7.0, 20 ], + [ 10.0, 20 ], + [ 11.0, 20 ] ] + }); + + // from-to calcMode=discrete one keyTime only + testCases.push({ + 'attr' : { 'values': '20', + 'calcMode': 'discrete', + 'keyTimes': '0' }, + 'times': [ [ 0, 20 ], + [ 6.9, 20 ], + [ 7.0, 20 ], + [ 10.0, 20 ], + [ 11.0, 20 ] ] + }); + + // from-to calcMode=discrete one keyTime, mismatches no. values + testCases.push({ + 'attr' : { 'values': '10; 20', + 'calcMode': 'discrete', + 'keyTimes': '0' }, + 'times': [ [ 0, -100 ] ] + }); + + // to + testCases.push({ + 'attr' : { 'to': '100', + 'keyTimes': '0.0; 1.0' }, + 'times': [ [ 0, -100 ], + [ 7, 40 ] ] + }); + + // to -- bad number of keyTimes (too many) + testCases.push({ + 'attr' : { 'to': '100', + 'keyTimes': '0.0; 0.5; 1.0' }, + 'times': [ [ 2, -100 ] ] + }); + + // unfrozen to calcMode=discrete two keyTimes + testCases.push({ + 'attr' : { 'to': '100', + 'calcMode': 'discrete', + 'keyTimes': '0.0; 1.0', + 'fill': 'remove' }, + 'times': [ [ 0, -100 ], + [ 7, -100 ], + [ 10, -100 ], + [ 12, -100 ]] + }); + + // frozen to calcMode=discrete two keyTimes + testCases.push({ + 'attr' : { 'to': '100', + 'calcMode': 'discrete', + 'keyTimes': '0.0; 1.0' }, + 'times': [ [ 0, -100 ], + [ 7, -100 ], + [ 10, 100 ], + [ 12, 100 ] ] + }); + + // to calcMode=discrete -- bad number of keyTimes (one, expecting two) + testCases.push({ + 'attr' : { 'to': '100', + 'calcMode': 'discrete', + 'keyTimes': '0' }, + 'times': [ [ 0, -100 ], + [ 7, -100 ] ] + }); + + // values calcMode=discrete + testCases.push({ + 'attr' : { 'values': '0; 10; 20; 30', + 'calcMode': 'discrete', + 'keyTimes': '0;.2;.4;.6' }, + 'times': [ [ 0, 0 ], + [ 1.9, 0 ], + [ 2, 10 ], + [ 3.9, 10 ], + [ 4.0, 20 ], + [ 5.9, 20 ], + [ 6.0, 30 ], + [ 9.9, 30 ], + [ 10.0, 30 ] ] + }); + + // The following two accumulate tests are from SMIL 3.0 + // (Note that this behaviour differs from that defined for SVG Tiny 1.2 which + // specifically excludes the last value: "Note that in the case of discrete + // animation, the frozen value that is used is the value of the animation just + // before the end of the active duration.") + // accumulate=none + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'calcMode': 'discrete', + 'keyTimes': '0;.5;1', + 'fill': 'freeze', + 'repeatCount': '2', + 'accumulate': 'none' }, + 'times': [ [ 0, 0 ], + [ 5, 10 ], + [ 10, 0 ], + [ 15, 10 ], + [ 20, 20 ], + [ 25, 20 ] ] + }); + + // accumulate=sum + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'calcMode': 'discrete', + 'keyTimes': '0;.5;1', + 'fill': 'freeze', + 'repeatCount': '2', + 'accumulate': 'sum' }, + 'times': [ [ 0, 0 ], + [ 5, 10 ], + [ 10, 20 ], + [ 15, 30 ], + [ 20, 40 ], + [ 25, 40 ] ] + }); + + // If the interpolation mode is paced, the keyTimes attribute is ignored. + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'calcMode': 'paced', + 'keyTimes': '0;.2;1' }, + 'times': [ [ 0, 0 ], + [ 2, 4 ], + [ 5, 10 ] ] + }); + + // SMIL 3 has: + // If the simple duration is indefinite and the interpolation mode is + // linear or spline, any keyTimes specification will be ignored. + // However, since keyTimes represent "a proportional offset into the simple + // duration of the animation element" surely discrete animation too cannot use + // keyTimes when the simple duration is indefinite. Hence SVGT 1.2 is surely + // more correct when it has: + // If the simple duration is indefinite, any 'keyTimes' specification will + // be ignored. + // (linear) + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'dur': 'indefinite', + 'keyTimes': '0;.2;1' }, + 'times': [ [ 0, 0 ], + [ 5, 0 ] ] + }); + // (spline) + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'dur': 'indefinite', + 'calcMode': 'spline', + 'keyTimes': '0;.2;1', + 'keySplines': '0 0 1 1; 0 0 1 1' }, + 'times': [ [ 0, 0 ], + [ 5, 0 ] ] + }); + // (discrete) + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'dur': 'indefinite', + 'calcMode': 'discrete', + 'keyTimes': '0;.2;1' }, + 'times': [ [ 0, 0 ], + [ 5, 0 ] ] + }); + + for (var i = 0; i < testCases.length; i++) { + gSvg.setCurrentTime(0); + var test = testCases[i]; + + // Create animation elements + var anim = createAnim(test.attr); + + // Run samples + for (var j = 0; j < test.times.length; j++) { + var times = test.times[j]; + gSvg.setCurrentTime(times[0]); + checkSample(anim, times[1], times[0], i); + } + + anim.parentNode.removeChild(anim); + } + + // fallback to discrete for non-additive animation + var attr = { 'values': 'butt; round; square', + 'attributeName': 'stroke-linecap', + 'calcMode': 'linear', + 'keyTimes': '0;.2;1', + 'fill': 'remove' }; + var anim = createAnim(attr); + var samples = [ [ 0, 'butt' ], + [ 1.9, 'butt' ], + [ 2.0, 'round' ], + [ 9.9, 'round' ], + [ 10, 'butt' ] // fill=remove so we'll never set it to square + ]; + for (var i = 0; i < samples.length; i++) { + var sample = samples[i]; + gSvg.setCurrentTime(sample[0]); + checkLineCapSample(anim, sample[1], sample[0], + "[non-interpolatable fallback]"); + } + anim.parentNode.removeChild(anim); + + SimpleTest.finish(); +} + +function parseOk(str, extra) +{ + var attr = { 'values': '0; 50; 100', + 'keyTimes': str }; + if (typeof(extra) == "object") { + for (name in extra) { + attr[name] = extra[name]; + } + } + return { + 'attr' : attr, + 'times': [ [ 0, 0 ] ] + }; +} + +function parseNotOk(str, extra) +{ + var result = parseOk(str, extra); + result.times = [ [ 0, -100 ] ]; + return result; +} + +function createAnim(attr) +{ + const svgns = "http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns, 'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','0s'); + anim.setAttribute('fill','freeze'); + for (name in attr) { + anim.setAttribute(name, attr[name]); + } + return document.getElementById('circle').appendChild(anim); +} + +function checkSample(anim, expectedValue, sampleTime, caseNum) +{ + var msg = "Test case " + caseNum + + " (keyTimes: '" + anim.getAttribute('keyTimes') + "'" + + " calcMode: " + anim.getAttribute('calcMode') + "), " + + "t=" + sampleTime + + ": Unexpected sample value:"; + is(anim.targetElement.cx.animVal.value, expectedValue, msg); +} + +function checkLineCapSample(anim, expectedValue, sampleTime, caseDescr) +{ + var msg = "Test case " + caseDescr + + " (keyTimes: '" + anim.getAttribute('keyTimes') + "'" + + " calcMode: " + anim.getAttribute('calcMode') + "), " + + "t=" + sampleTime + + ": Unexpected sample value:"; + var actualValue = + window.getComputedStyle(anim.targetElement, null). + getPropertyValue('stroke-linecap'); + is(actualValue, expectedValue, msg); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilKeyTimesPacedMode.xhtml b/dom/smil/test/test_smilKeyTimesPacedMode.xhtml new file mode 100644 index 000000000..eff537cfd --- /dev/null +++ b/dom/smil/test/test_smilKeyTimesPacedMode.xhtml @@ -0,0 +1,123 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Tests updated intervals</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=555026">Mozilla Bug 555026</a> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle r="10" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test that we ignore keyTimes attr when calcMode="paced" **/ + +/* Global Variables */ +const SVGNS = "http://www.w3.org/2000/svg"; +const ANIM_DUR = "2s"; +const HALF_TIME = "1"; +const ATTR_NAME = "cx" +const KEYTIMES_TO_TEST = [ + // potentially-valid values (depending on number of values in animation) + "0; 0.2; 1", + "0; 0.5", + "0; 1", + // invalid values: + "", "abc", "-0.5", "0; 0.5; 1.01", "5" +]; +const gSvg = document.getElementById("svg"); +const gCircle = document.getElementById("circle"); + +SimpleTest.waitForExplicitFinish(); + + +// MAIN FUNCTIONS +function main() { + ok(gSvg.animationsPaused(), "should be paused by <svg> load handler"); + is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testByAnimation(); + testToAnimation(); + testValuesAnimation(); + SimpleTest.finish(); +} + +function testByAnimation() { + for (var i = 0; i < KEYTIMES_TO_TEST.length; i++) { + setupTest(); + var anim = createAnim(); + anim.setAttribute("by", "200"); + var curKeyTimes = KEYTIMES_TO_TEST[i]; + anim.setAttribute("keyTimes", curKeyTimes); + + gSvg.setCurrentTime(HALF_TIME); + is(gCircle.cx.animVal.value, 100, + "Checking animVal with 'by' and keyTimes='" + curKeyTimes + "'"); + + anim.parentNode.removeChild(anim); // clean up + } +} + +function testToAnimation() { + for (var i = 0; i < KEYTIMES_TO_TEST.length; i++) { + setupTest(); + var anim = createAnim(); + anim.setAttribute("to", "200"); + var curKeyTimes = KEYTIMES_TO_TEST[i]; + anim.setAttribute("keyTimes", curKeyTimes); + + gSvg.setCurrentTime(HALF_TIME); + is(gCircle.cx.animVal.value, 100, + "Checking animVal with 'to' and keyTimes='" + curKeyTimes + "'"); + + anim.parentNode.removeChild(anim); // clean up + } +} + +function testValuesAnimation() { + for (var i = 0; i < KEYTIMES_TO_TEST.length; i++) { + setupTest(); + var anim = createAnim(); + anim.setAttribute("values", "100; 110; 200"); + var curKeyTimes = KEYTIMES_TO_TEST[i]; + anim.setAttribute("keyTimes", curKeyTimes); + + gSvg.setCurrentTime(HALF_TIME); + is(gCircle.cx.animVal.value, 150, + "Checking animVal with 'values' and keyTimes='" + curKeyTimes + "'"); + + anim.parentNode.removeChild(anim); // clean up + } +} + +// HELPER FUNCTIONS +// Common setup code for each test function: seek to 0, and make sure +// the previous test cleaned up its animations. +function setupTest() { + gSvg.setCurrentTime(0); + if (gCircle.firstChild) { + ok(false, "Previous test didn't clean up after itself."); + } +} + +function createAnim() { + var anim = document.createElementNS(SVGNS,"animate"); + anim.setAttribute("attributeName", ATTR_NAME); + anim.setAttribute("dur", ANIM_DUR); + anim.setAttribute("begin", "0s"); + anim.setAttribute("calcMode", "paced"); + return gCircle.appendChild(anim); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilMappedAttrFromBy.xhtml b/dom/smil/test/test_smilMappedAttrFromBy.xhtml new file mode 100644 index 000000000..48fea5256 --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrFromBy.xhtml @@ -0,0 +1,51 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilMappedAttrList.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSFromBy.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is + more predictable. (otherwise, line-height varies depending on platform) + --> + <text x="20" y="20" style="line-height: 10px !important">testing 123</text> + <line/> + <marker/> + <filter><feDiffuseLighting/></filter> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var testBundles = convertCSSBundlesToMappedAttr(gFromByBundles); + testBundleList(testBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilMappedAttrFromTo.xhtml b/dom/smil/test/test_smilMappedAttrFromTo.xhtml new file mode 100644 index 000000000..86e647e29 --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrFromTo.xhtml @@ -0,0 +1,79 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilMappedAttrList.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSFromTo.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is + more predictable. (otherwise, line-height varies depending on platform) + --> + <text x="20" y="20">testing 123</text> + <line/> + <image/> + <marker/> + <clipPath><circle/></clipPath> + <filter><feFlood/></filter> + <filter><feDiffuseLighting/></filter> + <linearGradient><stop/></linearGradient> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function checkForUntestedAttributes(bundleList) +{ + // Create the set of all the attributes we know about + var attributeSet = {}; + for (attributeLabel in gMappedAttrList) { + // insert attribute + attributeSet[gMappedAttrList[attributeLabel].attrName] = null; + } + // Remove tested properties from the set + for (var bundleIdx in bundleList) { + var bundle = bundleList[bundleIdx]; + delete attributeSet[bundle.animatedAttribute.attrName]; + } + // Warn about remaining (untested) properties + for (var untestedProp in attributeSet) { + ok(false, "No tests for attribute '" + untestedProp + "'"); + } +} + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var testBundles = convertCSSBundlesToMappedAttr(gFromToBundles); + + // FIRST: Warn about any attributes that are missing tests + checkForUntestedAttributes(testBundles); + + // Run the actual tests + testBundleList(testBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilMappedAttrPaced.xhtml b/dom/smil/test/test_smilMappedAttrPaced.xhtml new file mode 100644 index 000000000..c56b3aeb7 --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrPaced.xhtml @@ -0,0 +1,46 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilMappedAttrList.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSPaced.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <text x="20" y="20">testing 123</text> + <marker/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var testBundles = convertCSSBundlesToMappedAttr(gPacedBundles); + testBundleList(testBundles, new SMILTimingData(1.0, 6.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilMinTiming.html b/dom/smil/test/test_smilMinTiming.html new file mode 100644 index 000000000..1a82f3c96 --- /dev/null +++ b/dom/smil/test/test_smilMinTiming.html @@ -0,0 +1,93 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=948245 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 948245</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=948245">Mozilla Bug 948245</a> +<p id="display"></p> +<div id="content"> +<svg id="svg" onload="this.pauseAnimations()"> + <rect fill="red" id="rect" x="0"> + <animate attributeName="x" to="100" id="animation" dur="100s" min="200s"/> + </rect> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + // The 'min' attribute introduces a kind of additional state into the SMIL + // model. If the 'min' attribute extends the active duration, the additional + // time between the amount of time the animation normally runs for (called the + // 'repeat duration') and the extended active duration is filled using the + // fill mode. + // + // Below we refer to this period of time between the end of the repeat + // duration and the end of the active duration as the 'extended period'. + // + // This test verifies that as we jump in and out of these states we produce + // the correct values. + // + // The test animation above produces an active interval that is longer than + // the 'repeating duration' of the animation. + var rect = $('rect'), + animation = $('animation'); + + // Animation doesn't start until onload + SimpleTest.waitForExplicitFinish(); + window.addEventListener("load", runTests, false); + + function runTests() { + ok($('svg').animationsPaused(), "should be paused by <svg> load handler"); + + // In the extended period (t=150s) we should not be animating or filling + // since the default fill mode is "none". + animation.ownerSVGElement.setCurrentTime(150); + is(rect.x.animVal.value, 0, + "Shouldn't fill in extended period with fill='none'"); + + // If we set the fill mode we should start filling. + animation.setAttribute("fill", "freeze"); + is(rect.x.animVal.value, 100, + "Should fill in extended period with fill='freeze'"); + + // If we unset the fill attribute we should stop filling. + animation.removeAttribute("fill"); + is(rect.x.animVal.value, 0, "Shouldn't fill after unsetting fill"); + + // If we jump back into the repeated interval (at t=50s) we should be + // animating. + animation.ownerSVGElement.setCurrentTime(50); + is(rect.x.animVal.value, 50, "Should be active in repeating interval"); + + // If we jump to the boundary at the start of the extended period we should + // not be filling (since we removed the fill attribute above). + animation.ownerSVGElement.setCurrentTime(100); + is(rect.x.animVal.value, 0, + "Shouldn't fill after seeking to boundary of extended period"); + + // If we apply a fill mode at this boundary point we should do regular fill + // behavior of using the last value in the interpolation range. + animation.setAttribute("fill", "freeze"); + is(rect.x.animVal.value, 100, + "Should fill at boundary to extended period"); + + // Check that if we seek past the interval we fill with the value at the end + // of the _repeat_duration_ not the value at the end of the + // _active_duration_. + animation.setAttribute("repeatCount", "1.5"); + animation.ownerSVGElement.setCurrentTime(225); + is(rect.x.animVal.value, 50, + "Should fill with the end of the repeat duration value"); + + SimpleTest.finish(); + } +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilRepeatDuration.html b/dom/smil/test/test_smilRepeatDuration.html new file mode 100644 index 000000000..3690a9566 --- /dev/null +++ b/dom/smil/test/test_smilRepeatDuration.html @@ -0,0 +1,139 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=948245 +--> +<head> + <meta charset="utf-8"> + <title>Test for repeat duration calculation (Bug 948245)</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" +href="https://bugzilla.mozilla.org/show_bug.cgi?id=948245">Mozilla Bug 948245</a> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" onload="this.pauseAnimations()"> + <rect> + <animate id="a"/> + <animate id="b" begin="a.end"/> + </rect> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + // Tests the calculation of the repeat duration which is one of the steps + // towards determining the active duration. + // + // The repeat duration is determined by the following three attributes: + // + // dur: may be definite (e.g. '2s') or 'indefinite' (the default) + // repeatCount: may be definite (e.g. '2.5'), 'indefinite', or not set + // repeatDur: may be definite (e.g. '5s'), 'indefinite', or not set + // + // That leaves 18 combinations to test. + var testCases = + [ + // 1. repeatDur: definite, repeatCount: definite, dur: definite + // (Two test cases here to ensure we get the minimum) + { repeatDur: 15, repeatCount: 2, dur: 10, result: 15 }, + { repeatDur: 25, repeatCount: 2, dur: 10, result: 20 }, + // 2. repeatDur: indefinite, repeatCount: definite, dur: definite + { repeatDur: 'indefinite', repeatCount: 2, dur: 10, result: 20 }, + // 3. repeatDur: not set, repeatCount: definite, dur: definite + { repeatCount: 2, dur: 10, result: 20 }, + // 4. repeatDur: definite, repeatCount: indefinite, dur: definite + { repeatDur: 15, repeatCount: 'indefinite', dur: 10, result: 15 }, + // 5. repeatDur: indefinite, repeatCount: indefinite, dur: definite + { repeatDur: 'indefinite', repeatCount: 'indefinite', dur: 10, + result: 'indefinite' }, + // 6. repeatDur: not set, repeatCount: indefinite, dur: definite + { repeatCount: 'indefinite', dur: 10, result: 'indefinite' }, + // 7. repeatDur: definite, repeatCount: not set, dur: definite + { repeatDur: 15, dur: 10, result: 15 }, + // 8. repeatDur: indefinite, repeatCount: not set, dur: definite + { repeatDur: 'indefinite', dur: 10, result: 'indefinite' }, + // 9. repeatDur: not set, repeatCount: not set, dur: definite + { dur: 10, result: 10 }, + // 10. repeatDur: definite, repeatCount: definite, dur: indefinite + { repeatDur: 15, repeatCount: 2, dur: 'indefinite', result: 15 }, + // 11. repeatDur: indefinite, repeatCount: definite, dur: indefinite + { repeatDur: 'indefinite', repeatCount: 2, dur: 'indefinite', + result: 'indefinite' }, + // 12. repeatDur: not set, repeatCount: definite, dur: indefinite + { repeatCount: 2, dur: 'indefinite', result: 'indefinite' }, + // 13. repeatDur: definite, repeatCount: indefinite, dur: indefinite + { repeatDur: 15, repeatCount: 'indefinite', dur: 'indefinite', + result: 15 }, + // 14. repeatDur: indefinite, repeatCount: indefinite, dur: indefinite + { repeatDur: 'indefinite', repeatCount: 'indefinite', dur: 'indefinite', + result: 'indefinite' }, + // 15. repeatDur: not set, repeatCount: indefinite, dur: indefinite + { repeatCount: 'indefinite', dur: 'indefinite', result: 'indefinite' }, + // 16. repeatDur: definite, repeatCount: not set, dur: indefinite + { repeatDur: 15, dur: 'indefinite', result: 15 }, + // 17. repeatDur: indefinite, repeatCount: not set, dur: indefinite + { repeatDur: 'indefinite', dur: 'indefinite', result: 'indefinite' }, + // 18. repeatDur: not set, repeatCount: not set, dur: indefinite + { dur: 'indefinite', result: 'indefinite' } + ]; + + // We can test the repeat duration by setting these attributes on animation + // 'a' and checking the start time of 'b' which is defined to start when 'a' + // finishes. + // + // Since 'a' has no end/min/max attributes the end of its active interval + // should coincide with the end of its repeat duration. + // + // Sometimes the repeat duration is defined to be 'indefinite'. In this case + // calling getStartTime on b will throw an exception so we need to catch that + // exception and translate it to 'indefinite' as follows: + function getRepeatDuration() { + try { + return $('b').getStartTime(); + } catch(e) { + if (e.name == "InvalidStateError" && + e.code == DOMException.INVALID_STATE_ERR) { + return 'indefinite'; + } else { + ok(false, "Unexpected exception: " + e); + return null; + } + } + } + + // Animation doesn't start until onload + SimpleTest.waitForExplicitFinish(); + window.addEventListener("load", runTests, false); + + // Run through each of the test cases + function runTests() { + ok($('svg').animationsPaused(), "should be paused by <svg> load handler"); + + testCases.forEach(function(test) { + var a = $('a'); + + // Set the attributes + var msgPieces = []; + [ 'repeatDur', 'repeatCount', 'dur' ].forEach(function(attr) { + if (typeof test[attr] != "undefined") { + a.setAttribute(attr, test[attr].toString()); + msgPieces.push(attr + ': ' + test[attr].toString()); + } else { + a.removeAttribute(attr); + msgPieces.push(attr + ': <not set>'); + } + }); + var msg = msgPieces.join(', '); + + // Check the result + is(getRepeatDuration(), test.result, msg); + }); + + SimpleTest.finish(); + } +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilRepeatTiming.xhtml b/dom/smil/test/test_smilRepeatTiming.xhtml new file mode 100644 index 000000000..74a9c17af --- /dev/null +++ b/dom/smil/test/test_smilRepeatTiming.xhtml @@ -0,0 +1,96 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=485157 +--> +<head> + <title>Test repeat timing</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=485157">Mozilla Bug + 485157</a> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="100px" height="100px"> + <rect width="100" height="100" fill="green"> + <set attributeName="width" to="100" dur="20s" repeatCount="5" begin="0s" + id="a" onrepeat="startWaiting(evt)"/> + <set attributeName="fill" attributeType="CSS" to="green" + begin="a.repeat(1)" onbegin="expectedBegin()" dur="20s"/> + <set attributeName="x" to="100" + begin="a.repeat(2)" onbegin="unexpectedBegin(this)" dur="20s"/> + <set attributeName="y" to="100" + begin="a.repeat(0)" onbegin="unexpectedBegin(this)" dur="20s"/> + <set attributeName="width" to="100" + begin="a.repeat(-1)" onbegin="unexpectedBegin(this)" dur="20s"/> + <set attributeName="height" to="100" + begin="a.repeat(a)" onbegin="unexpectedBegin(this)" dur="20s"/> + </rect> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test SMIL repeat timing **/ + +/* Global Variables */ +const gTimeoutDur = 5000; // Time until we give up waiting for events in ms +var gSvg = document.getElementById('svg'); +var gRect = document.getElementById('circle'); +var gTimeoutID; +var gGotBegin = false; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +function testBegin() +{ + gSvg.setCurrentTime(19.999); +} + +function startWaiting(evt) +{ + is(evt.detail, 1, "Unexpected repeat event received: test broken"); + if (gGotBegin) + return; + + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); +} + +function timeoutFail() +{ + ok(false, "Timed out waiting for begin event"); + finish(); +} + +function expectedBegin() +{ + is(gGotBegin, false, + "Got begin event more than once for non-repeating animation"); + gGotBegin = true; + clearTimeout(gTimeoutID); + // Wait a moment before finishing in case there are erroneous events waiting + // to be processed. + setTimeout(finish, 10); +} + +function unexpectedBegin(elem) +{ + ok(false, "Got unexpected begin from animation with spec: " + + elem.getAttribute('begin')); +} + +function finish() +{ + gSvg.pauseAnimations(); + SimpleTest.finish(); +} + +window.addEventListener("load", testBegin, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilReset.xhtml b/dom/smil/test/test_smilReset.xhtml new file mode 100644 index 000000000..272d5cc0a --- /dev/null +++ b/dom/smil/test/test_smilReset.xhtml @@ -0,0 +1,82 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Tests for SMIL Reset Behavior </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" from="20" to="100" begin="2s" dur="4s" + id="anim1" attributeType="XML"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Tests for SMIL Reset Behavior **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = document.getElementById("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var anim = document.getElementById("anim1"); + is(anim.getStartTime(), 2, "Unexpected initial start time"); + + svg.setCurrentTime(1); + anim.beginElementAt(2); + + // We now have two instance times: 2, 3 + + // Restart (and reset) animation at t=1 + anim.beginElement(); + + // Instance times should now be 1, 2 (3 should have be reset) + is(anim.getStartTime(), 1, + "Unexpected start time after restart. Perhaps the added instance time " + + "was cleared"); + svg.setCurrentTime(4); + // Instance times will now be 2 (1 will have be reset when we restarted) + is(anim.getStartTime(), 2, "Unexpected start time after seek"); + + // Create a two new instance times at t=4, 5 + anim.beginElement(); + anim.beginElementAt(1); + is(anim.getStartTime(), 4, "Unexpected start time after beginElement"); + + // Here is a white box test to make sure we don't discard instance times + // created by DOM calls when setting/unsetting the 'begin' spec + anim.removeAttribute('begin'); + is(anim.getStartTime(), 4, "Unexpected start time after clearing begin spec"); + svg.setCurrentTime(6); + is(anim.getStartTime(), 5, + "Second DOM instance time cleared when begin spec was removed"); + + // And likewise, when we set it again + anim.beginElementAt(1); // Instance times now t=5s, 7s + anim.setAttribute('begin', '1s'); // + t=1s + is(anim.getStartTime(), 5, "Unexpected start time after setting begin spec"); + svg.setCurrentTime(8); + is(anim.getStartTime(), 7, + "Second DOM instance time cleared when begin spec was added"); + + // But check we do update state appropriately + anim.setAttribute('begin', '8s'); + is(anim.getStartTime(), 8, "Interval not updated with updated begin spec"); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilRestart.xhtml b/dom/smil/test/test_smilRestart.xhtml new file mode 100644 index 000000000..a4eab2e8c --- /dev/null +++ b/dom/smil/test/test_smilRestart.xhtml @@ -0,0 +1,102 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL Restart Behavior </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <!-- These 3 circles only differ in their animation's "restart" value --> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" from="20" to="100" begin="1s" dur="4s" + restart="always" id="always" attributeType="XML"/> + </circle> + <circle cx="20" cy="60" r="15" fill="blue"> + <animate attributeName="cx" from="20" to="100" begin="1s" dur="4s" + restart="whenNotActive" id="whenNotActive" attributeType="XML"/> + </circle> + <circle cx="20" cy="100" r="15" fill="blue"> + <animate attributeName="cx" from="20" to="100" begin="1s" dur="4s" + restart="never" id="never" attributeType="XML"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL Restart Behavior **/ + +/* Global Variables */ +var svg = document.getElementById("svg"); +var always = document.getElementById("always"); +var whenNotActive = document.getElementById("whenNotActive"); +var never = document.getElementById("never"); + +SimpleTest.waitForExplicitFinish(); + +function tryRestart(elem, state, expected) { + var restartTime = svg.getCurrentTime(); + elem.beginElement(); + var restart = false; + try { + restart = (elem.getStartTime() === restartTime); + } catch (e) { + if (e.name != "InvalidStateError" || + e.code != DOMException.INVALID_STATE_ERR) + throw e; + restart = false; + } + if (expected) { + var msg = elem.id + " can't restart in " + state + " state"; + ok(restart, msg); + } else { + var msg = elem.id + " can restart in " + state + " state"; + ok(!restart, msg); + } +} + +function main() { + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // At first everything should be starting at 1s + is(always.getStartTime(), 1); + is(whenNotActive.getStartTime(), 1); + is(never.getStartTime(), 1); + + // Now try to restart everything early, should be allowed by all + tryRestart(always, "waiting", true); + tryRestart(whenNotActive, "waiting", true); + tryRestart(never, "waiting", true); + + // Now skip to half-way + var newTime = always.getStartTime() + 0.5 * always.getSimpleDuration(); + svg.setCurrentTime(newTime); + + // Only 'always' should be able to be restarted + tryRestart(always, "active", true); + tryRestart(whenNotActive, "active", false); + tryRestart(never, "active", false); + + // Now skip to the end + newTime = always.getStartTime() + always.getSimpleDuration() + 1; + svg.setCurrentTime(newTime); + + // All animations have finished, so 'always' and 'whenNotActive' should be + // able to be restarted + tryRestart(always, "postactive", true); + tryRestart(whenNotActive, "postactive", true); + tryRestart(never, "postactive", false); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilSetCurrentTime.xhtml b/dom/smil/test/test_smilSetCurrentTime.xhtml new file mode 100644 index 000000000..36f64a49f --- /dev/null +++ b/dom/smil/test/test_smilSetCurrentTime.xhtml @@ -0,0 +1,76 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for setCurrentTime Behavior </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" + onload="this.pauseAnimations()" /> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for basic setCurrentTime / getCurrentTime Behavior **/ + +/* Global Variables & Constants */ +const PRECISION_LEVEL = 0.0000001; // Allow small level of floating-point error +const gTimes = [0, 1.5, 0.2, 0.99, -400.5, 10000000, -1]; +const gWaitTime = 20; +var gSvg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +function main() { + ok(gSvg.animationsPaused(), "should be paused by <svg> load handler"); + is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // Test that seeking takes effect immediately + for (var i = 0; i < gTimes.length; i++) { + gSvg.setCurrentTime(gTimes[i]); + // We adopt the SVGT1.2 behavior of clamping negative times to 0 + assertFloatsEqual(gSvg.getCurrentTime(), Math.max(gTimes[i], 0.0)); + } + + // Test that seeking isn't messed up by timeouts + // (using tail recursion to set up the chain of timeout function calls) + var func = function() { + checkTimesAfterIndex(0); + } + setTimeout(func, gWaitTime); +} + +/* This method seeks to the time at gTimes[index], + * and then sets up a timeout to... + * - verify that the seek worked + * - make a recursive call for the next index. + */ +function checkTimesAfterIndex(index) { + if (index == gTimes.length) { + // base case -- we're done! + SimpleTest.finish(); + return; + } + + gSvg.setCurrentTime(gTimes[index]); + var func = function() { + assertFloatsEqual(gSvg.getCurrentTime(), Math.max(gTimes[index], 0.0)); + checkTimesAfterIndex(index + 1); + } + setTimeout(func, gWaitTime); +} + +function assertFloatsEqual(aVal, aExpected) { + ok(Math.abs(aVal - aExpected) <= PRECISION_LEVEL, + "getCurrentTime returned " + aVal + " after seeking to " + aExpected) +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilSync.xhtml b/dom/smil/test/test_smilSync.xhtml new file mode 100644 index 000000000..0db9dd5a2 --- /dev/null +++ b/dom/smil/test/test_smilSync.xhtml @@ -0,0 +1,255 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL sync behaviour </title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="20" to="100" + begin="indefinite" dur="4s" restart="always" id="anim1"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="0" to="50" + begin="0" dur="1s" additive="sum" fill="freeze" id="anim2"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="0" to="50" + begin="0" dur="10s" additive="sum" fill="freeze" id="anim3"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="0" to="50" + begin="0" dur="10s" additive="sum" fill="freeze" id="anim4"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="0" to="50" + begin="0" dur="40s" additive="sum" fill="freeze" id="anim5"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="20" to="100" + begin="100s" dur="4s" restart="always" id="anim6"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL sync behavior **/ + +/* Global Variables */ +var svg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); + +function main() { + testBeginAt(document.getElementById("anim1")); + testChangeBaseVal(document.getElementById("anim2")); + testChangeWhilePaused(document.getElementById("anim3")); + testChangeAnimAttribute(document.getElementById("anim4")); + testChangeTimingAttribute(document.getElementById("anim5")); + testSetCurrentTime(document.getElementById("anim6")); + SimpleTest.finish(); +} + +function testBeginAt(anim) { + // This (hugely important) test checks that a call to beginElement updates to + // the new interval + + // Check some pre-conditions + is(anim.getAttribute("restart"), "always"); + ok(anim.getSimpleDuration() >= 4); + + // First start the animation + svg.setCurrentTime(2); + anim.beginElement(); + + // Then restart it--twice + svg.setCurrentTime(4); + anim.beginElement(); + anim.beginElementAt(-1); + + // The first restart should win if the state machine has been successfully + // updated. If we get '3' back instead we haven't updated properly. + is(anim.getStartTime(), 4); +} + +function testChangeBaseVal(anim) { + // Check that a change to the base value is updated even after animation is + // frozen + + // preconditions -- element should have ended + try { + anim.getStartTime(); + ok(false, "Element has not ended yet."); + } catch (e) { } + + // check frozen value is applied + var target = anim.targetElement; + is(target.cx.animVal.value, 70); + is(target.cx.baseVal.value, 20); + + // change base val and re-check + target.cx.baseVal.value = 30; + is(target.cx.animVal.value, 80); + is(target.cx.baseVal.value, 30); +} + +function testChangeWhilePaused(anim) { + // Check that a change to the base value is updated even when the animation is + // paused + + svg.pauseAnimations(); + svg.setCurrentTime(anim.getSimpleDuration() / 2); + + // check paused value is applied + var target = anim.targetElement; + is(target.cx.animVal.value, 45); + is(target.cx.baseVal.value, 20); + + // change base val and re-check + target.cx.baseVal.value = 30; + is(target.cx.animVal.value, 55); + is(target.cx.baseVal.value, 30); +} + +function testChangeAnimAttribute(anim) { + // Check that a change to an animation attribute causes an update even when + // the animation is frozen and paused + + // Make sure animation is paused and frozen + svg.pauseAnimations(); + svg.setCurrentTime(anim.getStartTime() + anim.getSimpleDuration() + 1); + + // Check frozen value is applied + var target = anim.targetElement; + is(target.cx.animVal.value, 70); + is(target.cx.baseVal.value, 20); + + // Make the animation no longer additive + anim.removeAttribute("additive"); + is(target.cx.animVal.value, 50); + is(target.cx.baseVal.value, 20); +} + +function testChangeTimingAttribute(anim) { + // Check that a change to a timing attribute causes an update even when + // the animation is paused + + svg.pauseAnimations(); + svg.setCurrentTime(anim.getSimpleDuration() / 2); + + // Check part-way value is applied + var target = anim.targetElement; + is(target.cx.animVal.value, 45); + is(target.cx.baseVal.value, 20); + + // Make the animation no longer additive + anim.setAttribute("dur", String(anim.getSimpleDuration() / 2) + "s"); + is(target.cx.animVal.value, 70); + is(target.cx.baseVal.value, 20); + + // Remove fill + anim.removeAttribute("fill"); + is(target.cx.animVal.value, 20); + is(target.cx.baseVal.value, 20); +} + +function testSetCurrentTime(anim) { + // This test checks that a call to setCurrentTime flushes restarts + // + // Actually, this same scenario arises in test_smilRestart.xhtml but we + // isolate this particular situation here for easier diagnosis if this ever + // fails. + // + // At first we have: + // currentTime begin="100s" + // v v + // Doc time: 0---\/\/\/-------99----------100------- + // + svg.setCurrentTime(99); + is(anim.getStartTime(), 100); + + // Then we restart giving us: + // + // beginElement begin="100s" + // v v + // Doc time: 0---\/\/\/-------99----------100------- + // + // So our current interval is + // + // begin="100s" + // v + // +---------------| + // Doc time: 0---\/\/\/-------99-100-101-102-103----- + // + anim.beginElement(); + is(anim.getStartTime(), svg.getCurrentTime()); + + // Then we skip to half-way, i.e. + // + // currentTime + // v + // begin="100s" + // v + // +---------------| + // Doc time: 0---\/\/\/-------99-100-101-102-103----- + // + // At this point we should flush our restarts and early end the first interval + // and start the second interval, giving us + // + // So our timegraph looks like: + // + // currentTime + // v + // +---------------| + // +---| + // Doc time: 0---\/\/\/-------99-100-101-102-103-104- + // + var newTime = anim.getStartTime() + 0.5 * anim.getSimpleDuration(); + svg.setCurrentTime(newTime); + + // Finally we call beginElement again giving us + // + // currentTime + // v + // +---------------| + // +---| + // +---| + // Doc time: 0---\/\/\/-------99-100-101-102-103-104-105- + // + // If, however, setCurrentTime failed to flush restarts out starting point + // we do come to update the timegraph would be: + // + // beginElementAt + // v + // begin="100s" + // v + // +---------------| + // Doc time: 0---\/\/\/-------99-100-101-102-103----- + // + // And as soon as we encountered the begin="100s" spec we'd do a restart + // according to the SMIL algorithms and a restart involves a reset which + // clears the instance times created by DOM calls and so we'd end up with + // just: + // + // currentTime + // v + // +---------------| + // +---| + // Doc time: 0---\/\/\/-------99-100-101-102-103-104- + // + // Which is probably not what the author intended. + // + anim.beginElement(); + is(anim.getStartTime(), svg.getCurrentTime()); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilSyncTransform.xhtml b/dom/smil/test/test_smilSyncTransform.xhtml new file mode 100644 index 000000000..65debc97c --- /dev/null +++ b/dom/smil/test/test_smilSyncTransform.xhtml @@ -0,0 +1,66 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL sync behaviour for transform types</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animateTransform attributeName="transform" type="rotate" + from="90" to="180" begin="0s" dur="2s" fill="freeze" + additive="sum" id="anim1"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animateTransform attributeName="transform" type="scale" + from="1" to="2" begin="2s" dur="2s" id="anim2"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL sync behavior for transform types **/ + +/* Global Variables */ +var svg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); + +function main() { + testChangeBaseVal(document.getElementById("anim1")); + SimpleTest.finish(); +} + +function testChangeBaseVal(anim) { + // Check that a change to the base value is updated even after animation is + // frozen + + var target = anim.targetElement; + + var baseList = target.transform.baseVal; + var animList = target.transform.animVal; + + // make sure element has ended + svg.setCurrentTime(anim.getSimpleDuration()); + + // check frozen value is applied + is(baseList.numberOfItems, 0); + is(animList.numberOfItems, 1); + + // change base val and re-check + var newTransform = svg.createSVGTransform(); + newTransform.setScale(1,2); + baseList.appendItem(newTransform); + is(baseList.numberOfItems, 1); + is(animList.numberOfItems, 2); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilSyncbaseTarget.xhtml b/dom/smil/test/test_smilSyncbaseTarget.xhtml new file mode 100644 index 000000000..979c15391 --- /dev/null +++ b/dom/smil/test/test_smilSyncbaseTarget.xhtml @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for syncbase targetting</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-20" cy="20" r="15" fill="blue" id="circle"> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="a"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" xml:id="b"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="あ"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="a.b"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="a-b"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="a:b"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="-a"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="0"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for syncbase targetting behavior **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = getElement("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testSpecs(); + testChangeId(); + testRemoveTimebase(); + + SimpleTest.finish(); +} + +function testSpecs() { + var anim = createAnim(); + + // Sanity check--initial state + ok(noStart(anim), "Unexpected initial value for indefinite start time."); + + var specs = [ [ 'a.begin', 2 ], + [ 'b.begin', 'todo' ], // xml:id support, bug 275196 + [ 'あ.begin', 2 ], // unicode id + [ ' a.begin ', 2 ], // whitespace + [ 'a\\.b.begin', 2 ], // escaping + [ 'a\\-b.begin', 2 ], // escaping + [ 'a:b.begin', 2 ], + // Invalid + [ '-a.begin', 'notok' ], // invalid XML ID + [ '\\-a.begin', 'notok' ], // invalid XML ID + [ '0.begin', 'notok' ], // invalid XML ID + [ '\xB7.begin', 'notok' ], // invalid XML ID + [ '\x7B.begin', 'notok' ], // invalid XML ID + [ '.begin', 'notok' ], + [ ' .end ', 'notok' ], + [ 'a.begin-5a', 'notok' ], + // Offsets + [ ' a.begin + 1min', 2 + 60 ], + [ ' a.begin-0.5s', 1.5 ], + ]; + for (var i = 0; i < specs.length; i++) { + var spec = specs[i][0]; + var expected = specs[i][1]; + anim.setAttribute('begin', spec); + try { + if (typeof(expected) == 'number') { + is(anim.getStartTime(), expected, + "Unexpected start time with spec: " + spec); + } else if (expected == 'todo') { + todo_is(anim.getStartTime(), 2,"Unexpected success with spec: " + spec); + } else { + anim.getStartTime(); + ok(false, "Unexpected success with spec: " + spec); + } + } catch(e) { + if (e.name == "InvalidStateError" && + e.code == DOMException.INVALID_STATE_ERR) { + if (typeof(expected) == 'number') + ok(false, "Failed with spec: " + spec); + else if (expected == 'todo') + todo(false, "Yet to implement: " + spec); + else + ok(true); + } else { + ok(false, "Unexpected exception: " + e + "(with spec: " + spec + ")"); + } + } + } + + anim.parentNode.removeChild(anim); +} + +function testChangeId() { + var anim = createAnim(); + + anim.setAttribute('begin', 'a.begin'); + is(anim.getStartTime(), 2, "Unexpected start time."); + + var a = getElement('a'); + a.setAttribute('id', 'a1'); + ok(noStart(anim), "Unexpected return value after changing target ID."); + + a.setAttribute('id', 'a'); + is(anim.getStartTime(), 2, + "Unexpected start time after resetting target ID."); + + anim.parentNode.removeChild(anim); +} + +function testRemoveTimebase() { + var anim = createAnim(); + anim.setAttribute('begin', 'a.begin'); + ok(!noStart(anim), "Unexpected start time before removing timebase."); + + var circle = getElement('circle'); + var a = getElement('a'); + // Sanity check + is(a, circle.firstElementChild, "Unexpected document structure"); + + // Remove timebase + a.parentNode.removeChild(a); + ok(noStart(anim), "Unexpected start time after removing timebase."); + + // Reinsert timebase + circle.insertBefore(a, circle.firstElementChild); + ok(!noStart(anim), "Unexpected start time after re-inserting timebase."); + + // Remove dependent element + anim.parentNode.removeChild(anim); + ok(noStart(anim), "Unexpected start time after removing dependent."); + + // Create a new dependent + var anim2 = createAnim(); + anim2.setAttribute('begin', 'a.begin'); + is(anim2.getStartTime(), 2, + "Unexpected start time after adding new dependent."); +} + +function createAnim() { + const svgns="http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('from','0'); + anim.setAttribute('to','100'); + anim.setAttribute('begin','indefinite'); + anim.setAttribute('dur','1s'); + return getElement('circle').appendChild(anim); +} + +function noStart(elem) { + var exceptionCaught = false; + + try { + elem.getStartTime(); + } catch(e) { + exceptionCaught = true; + is (e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is (e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime."); + } + + return exceptionCaught; +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilTextZoom.xhtml b/dom/smil/test/test_smilTextZoom.xhtml new file mode 100644 index 000000000..814b81f6e --- /dev/null +++ b/dom/smil/test/test_smilTextZoom.xhtml @@ -0,0 +1,89 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL Animation Behavior with textZoom</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> + <svg xmlns="http://www.w3.org/2000/svg" width="300px" height="200px" + onload="this.pauseAnimations()"> + <text y="100px" x="0px" style="font-size: 5px"> + abc + <animate attributeName="font-size" attributeType="CSS" fill="freeze" + from="20px" to="40px" begin="1s" dur="1s"/> + </text> + <rect y="100px" x="50px" style="stroke-width: 5px"> + <animate attributeName="stroke-width" attributeType="CSS" fill="freeze" + from="20px" to="40px" begin="1s" dur="1s"/> + </rect> + </svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +// Helper function +function verifyStyle(aNode, aPropertyName, aExpectedVal) +{ + var computedVal = SMILUtil.getComputedStyleSimple(aNode, aPropertyName); + is(computedVal, aExpectedVal, "computed value of " + aPropertyName); +} + +function main() +{ + // Start out pause + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // Set text zoom to 2x + var origTextZoom = SpecialPowers.getTextZoom(window); + SpecialPowers.setTextZoom(window, 2); + + try { + // Verify computed style values at various points during animation. + // * Correct behavior is for the computed values of 'font-size' to be + // the same as their corresponding specified values, since text zoom + // should not affect SVG text elements. + // * I also include tests for an identical animation of the "stroke-width" + // property, which should _not_ be affected by textZoom. + var text = document.getElementsByTagName("text")[0]; + var rect = document.getElementsByTagName("rect")[0]; + + verifyStyle(text, "font-size", "5px"); + verifyStyle(rect, "stroke-width", "5px"); + svg.setCurrentTime(1); + verifyStyle(text, "font-size", "20px"); + verifyStyle(rect, "stroke-width", "20"); + svg.setCurrentTime(1.5); + verifyStyle(text, "font-size", "30px"); + verifyStyle(rect, "stroke-width", "30"); + svg.setCurrentTime(2); + verifyStyle(text, "font-size", "40px"); + verifyStyle(rect, "stroke-width", "40"); + svg.setCurrentTime(3); + verifyStyle(text, "font-size", "40px"); + verifyStyle(rect, "stroke-width", "40"); + } catch (e) { + // If anything goes wrong, make sure we restore textZoom before bubbling + // the exception upwards, so that we don't mess up subsequent tests. + SpecialPowers.setTextZoom(window, origTextZoom); + + throw e; + } + + // We're done! Restore original text-zoom before finishing + SpecialPowers.setTextZoom(window, origTextZoom); + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilTimeEvents.xhtml b/dom/smil/test/test_smilTimeEvents.xhtml new file mode 100644 index 000000000..bf6924ddb --- /dev/null +++ b/dom/smil/test/test_smilTimeEvents.xhtml @@ -0,0 +1,337 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=572270 +--> +<head> + <title>Test TimeEvents dispatching</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=572270">Mozilla Bug + 572270</a> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="100px" height="100px"> + <g font-size="10px"> + <circle cx="0" cy="0" r="15" fill="blue" id="circle" + onbegin="parentHandler(evt)" onrepeat="parentHandler(evt)" + onend="parentHandler(evt)"> + <animate attributeName="cy" from="0" to="100" dur="60s" begin="2s" + id="anim" repeatCount="2" + onbegin="handleOnBegin(evt)" onrepeat="handleOnRepeat(evt)" + onend="handleOnEnd(evt)"/> + </circle> + </g> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test SMIL TimeEvents dispatching **/ + +/* Global Variables */ +const gTimeoutDur = 60000; // Time until we give up waiting for events in ms +var gSvg = document.getElementById("svg"); +var gAnim = document.getElementById('anim'); +var gCircle = document.getElementById('circle'); +var gExpectedEvents = new Array(); +var gTimeoutID; +var gTestStages = + [ testPlaybackBegin, + testPlaybackRepeat, + testPlaybackEnd, + testForwardsSeekToMid, + testForwardsSeekToNextInterval, + testForwardsSeekPastEnd, + testBackwardsSeekToMid, + testBackwardsSeekToStart, + testCreateEvent, + testRegistration + ]; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +function continueTest() +{ + if (gTestStages.length == 0) { + SimpleTest.finish(); + return; + } + gTestStages.shift()(); +} + +function testPlaybackBegin() +{ + // Test events are dispatched through normal playback + gSvg.pauseAnimations(); + gSvg.setCurrentTime(1.99); + gExpectedEvents.push("beginEvent", "beginEvent"); // Two registered handlers + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gSvg.unpauseAnimations(); +} + +function testPlaybackRepeat() +{ + gSvg.pauseAnimations(); + gSvg.setCurrentTime(61.99); + gExpectedEvents.push(["repeatEvent", 1], ["repeatEvent", 1]); + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gSvg.unpauseAnimations(); +} + +function testPlaybackEnd() +{ + gSvg.pauseAnimations(); + gSvg.setCurrentTime(121.99); + gExpectedEvents.push("endEvent", "endEvent"); + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gSvg.unpauseAnimations(); +} + +function testForwardsSeekToMid() +{ + gSvg.pauseAnimations(); + // Set animation parameters to something that repeats a lot + gSvg.setCurrentTime(0); + gAnim.setAttribute('begin', '2s; 102s'); + gAnim.setAttribute('dur', '15s'); + gAnim.setAttribute('repeatCount', '6'); + gSvg.setCurrentTime(46.99); + gExpectedEvents.push("beginEvent", "beginEvent", + ["repeatEvent", 3], ["repeatEvent", 3]); + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gSvg.unpauseAnimations(); +} + +function testForwardsSeekToNextInterval() +{ + // Skip to next interval -- we shouldn't get any additional begin or end + // events in between + gSvg.pauseAnimations(); + gSvg.setCurrentTime(131.99); + gExpectedEvents.push(["repeatEvent", 2], ["repeatEvent", 2]); + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gSvg.unpauseAnimations(); +} + +function testForwardsSeekPastEnd() +{ + gSvg.pauseAnimations(); + gSvg.setCurrentTime(200); + gExpectedEvents.push("endEvent", "endEvent"); + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gSvg.unpauseAnimations(); +} + +function testBackwardsSeekToMid() +{ + gSvg.pauseAnimations(); + gSvg.setCurrentTime(31.99); + gExpectedEvents.push("beginEvent", "beginEvent", + ["repeatEvent", 2], ["repeatEvent", 2]); + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gSvg.unpauseAnimations(); +} + +function testBackwardsSeekToStart() +{ + gSvg.pauseAnimations(); + gExpectedEvents.push("endEvent", "endEvent"); + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gSvg.setCurrentTime(0); +} + +function testCreateEvent() +{ + var evt; + try { + evt = document.createEvent("TimeEvents"); + } catch (e) { + ok(false, "Failed to create TimeEvent via script: " + e); + return; + } + evt.initTimeEvent("repeatEvent", null, 3); + is(evt.type, "repeatEvent", "Unexpected type for user-generated event"); + is(evt.detail, 3, "Unexpected detail for user-generated event"); + is(evt.target, null, "Unexpected event target"); + is(evt.currentTarget, null, "Unexpected event current target"); + is(evt.eventPhase, evt.NONE); + is(evt.bubbles, false, "Event should not bubble"); + is(evt.cancelable, false, "Event should not be cancelable"); + is(evt.view, null, "Event view should be null"); + + // Prior to dispatch we should be able to change the event type + evt.initTimeEvent("beginEvent", document.defaultView, 0); + is(evt.type, "beginEvent", "Failed to update event type before dispatch"); + is(evt.detail, 0, "Failed to update event detail before dispatch"); + is(evt.view, document.defaultView, "Event view should be set"); + + // But not directly as it's readonly + try { + evt.type = "endEvent"; + } catch(e) { } + is(evt.type, "beginEvent", "Event type should be readonly"); + + // Likewise the detail field should be readonly + try { + evt.detail = "8"; + } catch(e) { } + is(evt.detail, 0, "Event detail should be readonly"); + + // Dispatch + gExpectedEvents.push("beginEvent", "beginEvent"); + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gAnim.dispatchEvent(evt); +} + +function testRegistration() +{ + gSvg.pauseAnimations(); + // Reset animation to something simple + gSvg.setCurrentTime(0); + gAnim.setAttribute('begin', '2s'); + gAnim.setAttribute('dur', '50s'); + + // Remove attribute handler + gAnim.removeAttribute('onbegin'); + + // Add bogus handlers + gAnim.setAttribute('onbeginElement', 'handleOnBegin(evt)'); + gAnim.addEventListener("begin", handleOnBegin, false); + gAnim.addEventListener("onbegin", handleOnBegin, false); + + // We should now have just one legitimate listener: the one registered to + // handle 'beginElement' + gSvg.setCurrentTime(1.99); + gExpectedEvents.push("beginEvent"); + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); + gSvg.unpauseAnimations(); +} + +function handleOnBegin(evt) +{ + is(evt.type, "beginEvent", "Expected begin event but got " + evt.type); + checkExpectedEvent(evt); +} + +function handleOnRepeat(evt) +{ + is(evt.type, "repeatEvent", "Expected repeat event but got " + evt.type); + checkExpectedEvent(evt); +} + +function handleOnEnd(evt) +{ + is(evt.type, "endEvent", "Expected end event but got " + evt.type); + checkExpectedEvent(evt); +} + +function sanityCheckEvent(evt) +{ + is(evt.target, gAnim, "Unexpected event target"); + is(evt.currentTarget, gAnim, "Unexpected event current target"); + is(evt.eventPhase, evt.AT_TARGET); + is(evt.bubbles, false, "Event should not bubble"); + is(evt.cancelable, false, "Event should not be cancelable"); + if (SpecialPowers.getBoolPref("dom.event.highrestimestamp.enabled")) { + var now = window.performance.now(); + ok(evt.timeStamp > 0 && evt.timeStamp < now, + "Event timeStamp (" + evt.timeStamp + ") should be > 0 but " + + "before the current time (" + now + ")"); + } else { + is(evt.timeStamp, 0, "Event timeStamp should be 0"); + } + ok(evt.view !== null, "Event view not set"); +} + +function checkExpectedEvent(evt) +{ + sanityCheckEvent(evt); + ok(gExpectedEvents.length > 0, "Unexpected event: " + evt.type); + if (gExpectedEvents.length == 0) return; + + var expected = gExpectedEvents.shift(); + if (typeof expected == 'string') { + is(evt.type, expected, "Unexpected event type"); + is(evt.detail, 0, "Unexpected event detail (repeat iteration)"); + } else { + is(evt.type, expected[0], "Unexpected event type"); + is(evt.detail, expected[1], "Unexpected event detail (repeat iteration)"); + } + if (gExpectedEvents.length == 0) { + clearTimeout(gTimeoutID); + continueTest(); + } +} + +function timeoutFail() +{ + ok(false, "Timed out waiting for events: " + gExpectedEvents.join(', ')); + SimpleTest.finish(); // No point continuing +} + +function parentHandler(evt) +{ + ok(false, "Handler on parent got called but event shouldn't bubble."); +} + +window.addEventListener("load", continueTest, false); + +// Register event handlers *in addition* to the handlers already added via the +// "onbegin", "onend", "onrepeat" attributes on the <animate> and <circle> +// elements. This is to test that both types of registration work. +gAnim.addEventListener("beginEvent", handleOnBegin, false); +gAnim.addEventListener("repeatEvent", handleOnRepeat, false); +gAnim.addEventListener("endEvent", handleOnEnd, false); +gCircle.addEventListener("beginEvent", parentHandler, false); + +var expectedEvents = + ["begin", "beginEvent", "repeat", "repeatEvent", "end", "endEvent", "SVGZoom", "zoom"]; + +for (var i = 0; i < expectedEvents.length; ++i) { + is((new Event(expectedEvents[i])).type, expectedEvents[i], "Unexpected event type!"); +} + +var timeEvents = ["begin", "repeat", "end"]; +var expectedEvents = ["begin", "beginEvent", "repeat", "repeatEvent", "end", "endEvent"]; +var d = document.createElement("div"); +for (var i = 0; i < timeEvents.length; ++i) { + d.addEventListener(timeEvents[i], function(e) { + is(e.type, expectedEvents[0], "Got the expected event type."); + expectedEvents.shift(); + }); + + // Without "Event" suffix. + var e = document.createEvent("timeevent"); + e.initEvent(timeEvents[i], true, true); + d.dispatchEvent(e); + + // With "Event" suffix. + e = document.createEvent("timeevent"); + e.initEvent(timeEvents[i] + "Event", true, true); + d.dispatchEvent(e); +} +is(expectedEvents.length, 0, "Got all the expected events."); + +expectedEvents = ["zoom", "SVGZoom"]; +d.addEventListener("zoom", function(e) { + is(e.type, expectedEvents[0]); + expectedEvents.shift(); +}); + +var zoomEvent = document.createEvent("svgzoomevent"); +zoomEvent.initEvent("zoom", true, true); +d.dispatchEvent(zoomEvent); +zoomEvent = document.createEvent("svgzoomevent"); +zoomEvent.initEvent("SVGZoom", true, true); +d.dispatchEvent(zoomEvent); +is(expectedEvents.length, 0, "Got all the expected events."); + +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilTiming.xhtml b/dom/smil/test/test_smilTiming.xhtml new file mode 100644 index 000000000..cab1d4080 --- /dev/null +++ b/dom/smil/test/test_smilTiming.xhtml @@ -0,0 +1,291 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL timing</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL timing **/ + +/* Global Variables */ +const svgns = "http://www.w3.org/2000/svg"; +var gSvg = document.getElementById("svg"); +var gCircle = document.getElementById('circle'); + +SimpleTest.waitForExplicitFinish(); + +function main() { + ok(gSvg.animationsPaused(), "should be paused by <svg> load handler"); + is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var testCases = Array(); + + const secPerMin = 60; + const secPerHour = secPerMin * 60; + + // In the following tests that compare start times, getStartTime will round + // the start time to three decimal places since we expect our implementation + // to be millisecond accurate. + + // Offset syntax + // -- Basic tests, sign and whitespace + testCases.push(StartTimeTest('3s', 3)); + testCases.push(StartTimeTest('0s', 0)); + testCases.push(StartTimeTest('+2s', 2)); + testCases.push(StartTimeTest('-1s\t\r', -1)); + testCases.push(StartTimeTest('- 1s', -1)); + testCases.push(StartTimeTest(' -1s', -1)); + testCases.push(StartTimeTest(' - 1s', -1)); + testCases.push(StartTimeTest(' \t\n\r-1s', -1)); + testCases.push(StartTimeTest('+\n5s', 5)); + testCases.push(StartTimeTest('-\n5s', -5)); + testCases.push(StartTimeTest('\t 5s', 5)); + // -- These tests are from SMILANIM 3.6.7 + testCases.push(StartTimeTest('02:30:03', 2*secPerHour + 30*secPerMin + 3)); + testCases.push(StartTimeTest('50:00:10.25', 50*secPerHour + 10.25)); + testCases.push(StartTimeTest('02:33', 2*secPerMin + 33)); + testCases.push(StartTimeTest('00:10.5', 10.5)); + testCases.push(StartTimeTest('3.2h', 3.2*secPerHour)); + testCases.push(StartTimeTest('45min', 45*secPerMin)); + testCases.push(StartTimeTest('30s', 30)); + testCases.push(StartTimeTest('5ms', 0.005)); + testCases.push(StartTimeTest('12.467', 12.467)); + testCases.push(StartTimeTest('00.5s', 0.5)); + testCases.push(StartTimeTest('00:00.005', 0.005)); + // -- Additional tests + testCases.push(StartTimeTest('61:59:59', 61*secPerHour + 59*secPerMin + 59)); + testCases.push(StartTimeTest('02:59.999999999999999999999', 3*secPerMin)); + testCases.push(StartTimeTest('1234:23:45', + 1234*secPerHour + 23*secPerMin + 45)); + testCases.push(StartTimeTest('61min', 61*secPerMin)); + testCases.push(StartTimeTest('0:30:03', 30*secPerMin + 3)); + // -- Fractional precision + testCases.push(StartTimeTest('25.4567', 25.457)); + testCases.push(StartTimeTest('0.123456789', 0.123)); + testCases.push(StartTimeTest('0.00000000000000000000001', 0)); + testCases.push(StartTimeTest('-0.00000000000000000000001', 0)); + testCases.push(StartTimeTest('0.0009', 0.001)); + testCases.push(StartTimeTest('0.99999999999999999999999999999999999999', 1)); + testCases.push(StartTimeTest('23.4567ms', 0.023)); + testCases.push(StartTimeTest('23.7ms', 0.024)); + // -- Test errors + testCases.push(StartTimeTest(' + +3s', 'none')); + testCases.push(StartTimeTest(' +-3s', 'none')); + testCases.push(StartTimeTest('1:12:12:12', 'none')); + testCases.push(StartTimeTest('4:50:60', 'none')); + testCases.push(StartTimeTest('4:60:0', 'none')); + testCases.push(StartTimeTest('4:60', 'none')); + testCases.push(StartTimeTest('4:-1:00', 'none')); + testCases.push(StartTimeTest('4 5m', 'none')); + testCases.push(StartTimeTest('4 5ms', 'none')); + testCases.push(StartTimeTest('02:3:03', 'none')); + testCases.push(StartTimeTest('45.7 s', 'none')); + testCases.push(StartTimeTest(' 3 h ', 'none')); + testCases.push(StartTimeTest('2:33 ', 'none')); + testCases.push(StartTimeTest('02:33 2', 'none')); + testCases.push(StartTimeTest('\u000B 02:33', 'none')); + testCases.push(StartTimeTest('h', 'none')); + testCases.push(StartTimeTest('23.s', 'none')); + testCases.push(StartTimeTest('23.', 'none')); + testCases.push(StartTimeTest('23.54.2s', 'none')); + testCases.push(StartTimeTest('23sec', 'none')); + testCases.push(StartTimeTest('five', 'none')); + testCases.push(StartTimeTest('', 'none')); + testCases.push(StartTimeTest('02:33s', 'none')); + testCases.push(StartTimeTest('02:33 s', 'none')); + testCases.push(StartTimeTest('2.54e6', 'none')); + testCases.push(StartTimeTest('02.5:33', 'none')); + testCases.push(StartTimeTest('2:-45:33', 'none')); + testCases.push(StartTimeTest('2:4.5:33', 'none')); + testCases.push(StartTimeTest('45m', 'none')); + testCases.push(StartTimeTest(':20:30', 'none')); + testCases.push(StartTimeTest('1.5:30', 'none')); + testCases.push(StartTimeTest('15:-30', 'none')); + testCases.push(StartTimeTest('::30', 'none')); + testCases.push(StartTimeTest('15:30s', 'none')); + testCases.push(StartTimeTest('2:1.:30', 'none')); + testCases.push(StartTimeTest('2:.1:30', 'none')); + testCases.push(StartTimeTest('2.0:15:30', 'none')); + testCases.push(StartTimeTest('2.:15:30', 'none')); + testCases.push(StartTimeTest('.2:15:30', 'none')); + testCases.push(StartTimeTest('70:15', 'none')); + testCases.push(StartTimeTest('media', 'none')); + testCases.push(StartTimeTest('5mi', 'none')); + testCases.push(StartTimeTest('5hours', 'none')); + testCases.push(StartTimeTest('h05:30', 'none')); + testCases.push(StartTimeTest('05:40\x9A', 'none')); + testCases.push(StartTimeTest('05:40\u30D5', 'none')); + testCases.push(StartTimeTest('05:40β', 'none')); + + // List syntax + testCases.push(StartTimeTest('3', 3)); + testCases.push(StartTimeTest('3;', 3)); + testCases.push(StartTimeTest('3; ', 3)); + testCases.push(StartTimeTest('3 ; ', 3)); + testCases.push(StartTimeTest('3;;', 'none')); + testCases.push(StartTimeTest('3;; ', 'none')); + testCases.push(StartTimeTest(';3', 'none')); + testCases.push(StartTimeTest(' ;3', 'none')); + testCases.push(StartTimeTest('3;4', 3)); + testCases.push(StartTimeTest(' 3 ; 4 ', 3)); + + // List syntax on end times + testCases.push({ + 'attr' : { 'begin': '0s', + 'end': '1s; 2s' }, + 'times': [ [ 0, 0 ], + [ 1, -100 ] ] + }); + testCases.push({ + 'attr' : { 'begin': '0s', + 'end': '1s; 2s; ' }, + 'times': [ [ 0, 0 ], + [ 1, -100 ] ] + }); + testCases.push({ + 'attr' : { 'begin': '0s', + 'end': '3s; 2s' }, + 'times': [ [ 0, 0 ], + [ 1, 10 ], + [ 2, -100 ] ] + }); + + // Simple case + testCases.push({ + 'attr' : { 'begin': '3s' }, + 'times': [ [ 0, -100 ], + [ 4, 10 ] ] + }); + + // Multiple begins + testCases.push({ + 'attr' : { 'begin': '2s; 6s', + 'dur': '2s' }, + 'times': [ [ 0, -100 ], + [ 3, 50 ], + [ 4, -100 ], + [ 7, 50 ], + [ 8, -100 ] ] + }); + + // Negative begins + testCases.push({ + 'attr' : { 'begin': '-3s; 1s ; 4s', + 'dur': '2s ', + 'fill': 'freeze' }, + 'times': [ [ 0, -100 ], + [ 0.5, -100 ], + [ 1, 0 ], + [ 2, 50 ], + [ 3, 100 ], + [ 5, 50 ] ] + }); + + // Sorting + testCases.push({ + 'attr' : { 'begin': '-3s; 110s; 1s; 4s; -5s; -10s', + 'end': '111s; -5s; -15s; 6s; -5s; 1.2s', + 'dur': '2s ', + 'fill': 'freeze' }, + 'times': [ [ 0, -100 ], + [ 1, 0 ], + [ 2, 10 ], + [ 4, 0 ], + [ 5, 50 ], + [ 109, 100 ], + [ 110, 0 ], + [ 112, 50 ] ] + }); + + for (var i = 0; i < testCases.length; i++) { + gSvg.setCurrentTime(0); + var test = testCases[i]; + + // Generate string version of params for output messages + var params = ""; + for (var name in test.attr) { + params += name + '="' + test.attr[name] + '" '; + } + params = params.trim(); + + // Create animation elements + var anim = createAnim(test.attr); + + // Run samples + if ('times' in test) { + for (var j = 0; j < test.times.length; j++) { + var curSample = test.times[j]; + checkSample(curSample[0], curSample[1], params); + } + } + + // Check start time + if ('startTime' in test) { + is(getStartTime(anim), test.startTime, + "Got unexpected start time for " + params); + } + + anim.parentNode.removeChild(anim); + } + + SimpleTest.finish(); +} + +function createAnim(attr) { + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('from','0'); + anim.setAttribute('to','100'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','indefinite'); + for (name in attr) { + anim.setAttribute(name, attr[name]); + } + return gCircle.appendChild(anim); +} + +function checkSample(time, expectedValue, params) { + gSvg.setCurrentTime(time); + var msg = "Unexpected sample value for " + params + + " at t=" + time + ": "; + is(gCircle.cx.animVal.value, expectedValue); +} + +function getStartTime(anim) { + var startTime; + try { + startTime = anim.getStartTime(); + // We round start times to 3 decimal places to make comparisons simpler + startTime = parseFloat(startTime.toFixed(3)); + } catch(e) { + if (e.name == "InvalidStateError" && + e.code == DOMException.INVALID_STATE_ERR) { + startTime = 'none'; + } else { + ok(false, "Unexpected exception: " + e); + } + } + return startTime; +} + +function StartTimeTest(beginSpec, expectedStartTime) { + return { 'attr' : { 'begin': beginSpec }, + 'startTime': expectedStartTime }; +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilTimingZeroIntervals.xhtml b/dom/smil/test/test_smilTimingZeroIntervals.xhtml new file mode 100644 index 000000000..610cb5798 --- /dev/null +++ b/dom/smil/test/test_smilTimingZeroIntervals.xhtml @@ -0,0 +1,285 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL timing with zero-duration intervals</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL timing with zero-duration intervals **/ + +/* Global Variables */ +const svgns="http://www.w3.org/2000/svg"; +var svg = document.getElementById("svg"); +var circle = document.getElementById('circle'); + +SimpleTest.waitForExplicitFinish(); + +function createAnim() { + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('from','0'); + anim.setAttribute('to','100'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','indefinite'); + return circle.appendChild(anim); +} + +function removeAnim(anim) { + anim.parentNode.removeChild(anim); +} + +function main() { + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var tests = + [ testZeroDurationIntervalsA, + testZeroDurationIntervalsB, + testZeroDurationIntervalsC, + testZeroDurationIntervalsD, + testZeroDurationIntervalsE, + testZeroDurationIntervalsF, + testZeroDurationIntervalsG, + testZeroDurationIntervalsH, + testZeroDurationIntervalsI, + testZeroDurationIntervalsJ, + testZeroDurationIntervalsK, + testZeroDurationIntervalsL, + testZeroDurationIntervalsM, + testZeroDurationIntervalsN, + testZeroDurationIntervalsO + ]; + for (var i = 0; i < tests.length; i++) { + var anim = createAnim(); + svg.setCurrentTime(0); + tests[i](anim); + removeAnim(anim); + } + SimpleTest.finish(); +} + +function checkSample(time, expectedValue) { + svg.setCurrentTime(time); + is(circle.cx.animVal.value, expectedValue); +} + +function testZeroDurationIntervalsA(anim) { + // The zero-duration interval should play, followed by a second interval + // starting at the same point. There is no end for the interval + // at 4s so it should not play. + anim.setAttribute('begin', '1s ;4s'); + anim.setAttribute('end', '1s; 2s'); + anim.setAttribute('dur', '2s '); + anim.setAttribute('fill', 'freeze'); + checkSample(0,-100); + checkSample(1,0); + checkSample(1.1,5); + checkSample(2,50); + checkSample(3,50); + checkSample(4,50); + checkSample(5,50); + checkSample(6,50); +} + +function testZeroDurationIntervalsB(anim) { + // This interval should however actually restart as there is a valid end-point + anim.setAttribute('begin', '1s ;4s'); + anim.setAttribute('end', '1.1s; indefinite'); + anim.setAttribute('dur', '2s '); + anim.setAttribute('fill', 'freeze'); + checkSample(0,-100); + checkSample(1,0); + checkSample(1.1,5); + checkSample(2,5); + checkSample(4,0); + checkSample(5,50); +} + +function testZeroDurationIntervalsC(anim) { + // -0.5s has already been used as the endpoint of one interval so don't use it + // a second time + anim.setAttribute('begin', '-2s; -0.5s'); + anim.setAttribute('end', '-0.5s; 1s'); + anim.setAttribute('dur', '2s'); + anim.setAttribute('fill', 'freeze'); + checkSample(0,25); + checkSample(1.5,75); +} + +function testZeroDurationIntervalsD(anim) { + // Two end points that could make a zero-length interval + anim.setAttribute('begin', '-2s; -0.5s'); + anim.setAttribute('end', '-0.5s; -0.5s; 1s'); + anim.setAttribute('dur', '2s'); + anim.setAttribute('fill', 'freeze'); + checkSample(0,25); + checkSample(1.5,75); +} + +function testZeroDurationIntervalsE(anim) { + // Should give us 1s-1s, 1s-5s + anim.setAttribute('begin', '1s'); + anim.setAttribute('end', '1s; 5s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(6,40); +} + +function testZeroDurationIntervalsF(anim) { + // Should give us 1s-1s + anim.setAttribute('begin', '1s'); + anim.setAttribute('end', '1s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,0); + try { + anim.getStartTime(); + ok(false, "Failed to throw exception when there's no current interval."); + } catch (e) { } +} + +function testZeroDurationIntervalsG(anim) { + // Test a non-zero interval after a zero interval + // Should give us 1-2s, 3-3s, 3-4s + anim.setAttribute('begin', '1s; 3s'); + anim.setAttribute('end', '3s; 5s'); + anim.setAttribute('dur', '1s'); + anim.setAttribute('fill', 'freeze'); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,100); + checkSample(3,0); + checkSample(5,100); +} + +function testZeroDurationIntervalsH(anim) { + // Test multiple non-adjacent zero-intervals + // Should give us 1-1s, 1-2s, 3-3s, 3-4s + anim.setAttribute('begin', '1s; 3s'); + anim.setAttribute('end', '1s; 3s; 5s'); + anim.setAttribute('dur', '1s'); + anim.setAttribute('fill', 'freeze'); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,100); + checkSample(3,0); + checkSample(5,100); +} + +function testZeroDurationIntervalsI(anim) { + // Test skipping values that are the same + // Should give us 1-1s, 1-2s + anim.setAttribute('begin', '1s; 1s'); + anim.setAttribute('end', '1s; 1s; 2s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,10); + checkSample(3,10); +} + +function testZeroDurationIntervalsJ(anim) { + // Should give us 0-0.5s, 1-1s, 1-3s + anim.setAttribute('begin', '0s; 1s; 1s'); + anim.setAttribute('end', '1s; 3s'); + anim.setAttribute('dur', '0.5s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),0); + checkSample(0,0); + checkSample(0.6,100); + checkSample(1,0); + checkSample(2,100); +} + +function testZeroDurationIntervalsK(anim) { + // Should give us -0.5-1s + anim.setAttribute('begin', '-0.5s'); + anim.setAttribute('end', '-0.5s; 1s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),-0.5); + checkSample(0,5); + checkSample(1,15); + checkSample(2,15); +} + +function testZeroDurationIntervalsL(anim) { + // Test that multiple end values are ignored + // Should give us 1-1s, 1-3s + anim.setAttribute('begin', '1s'); + anim.setAttribute('end', '1s; 1s; 1s; 3s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,10); + checkSample(4,20); +} + +function testZeroDurationIntervalsM(anim) { + // Test 0-duration interval at start + anim.setAttribute('begin', '0s'); + anim.setAttribute('end', '0s'); + anim.setAttribute('fill', 'freeze'); + try { + anim.getStartTime(); + ok(false, "Failed to throw exception when there's no current interval."); + } catch (e) { } + checkSample(0,0); + checkSample(1,0); +} + +function testZeroDurationIntervalsN(anim) { + // Test 0-active-duration interval at start (different code path to above) + anim.setAttribute('begin', '0s'); + anim.setAttribute('repeatDur', '0s'); + anim.setAttribute('fill', 'freeze'); + try { + anim.getStartTime(); + ok(false, "Failed to throw exception when there's no current interval."); + } catch (e) { } + checkSample(0,0); + checkSample(1,0); +} + +function testZeroDurationIntervalsO(anim) { + // Make a zero-duration interval by constraining the active duration + // We should not loop infinitely but should look for the next begin time after + // that (in this case that is 2s, which would otherwise have been skipped + // because restart=whenNotActive) + // Should give us 1-1s, 2-2s + anim.setAttribute('begin', '1s; 2s'); + anim.setAttribute('repeatDur', '0s'); + anim.setAttribute('restart', 'whenNotActive'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(1.5,0); + checkSample(3,0); + try { + anim.getStartTime(); + ok(false, "Failed to throw exception when there's no current interval."); + } catch (e) { } +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilUpdatedInterval.xhtml b/dom/smil/test/test_smilUpdatedInterval.xhtml new file mode 100644 index 000000000..26b793dc6 --- /dev/null +++ b/dom/smil/test/test_smilUpdatedInterval.xhtml @@ -0,0 +1,64 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Tests updated intervals</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="20" cy="20" r="15" fill="blue" id="circle"> + <animate attributeName="cx" from="0" to="100" begin="2s" dur="4s" + id="anim1" attributeType="XML"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Tests for updated intervals **/ + +/* Global Variables */ +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = document.getElementById("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var anim = document.getElementById("anim1"); + + // Check regular operation + svg.setCurrentTime(3); + is(anim.getStartTime(), 2, "Unexpected initial start time"); + + // Add an instance time before the current interval at t=1s + anim.beginElementAt(-2); + + // We shouldn't change the begin time + is(anim.getStartTime(), 2, "Start time shouldn't have changed"); + + // Or the end--that is, if we go to t=5.5 we should still be running + svg.setCurrentTime(5.5); + try { + is(anim.getSimpleDuration(), 4, "Simple duration shouldn't have changed"); + is(anim.getStartTime(), 2, "Start time shouldn't have changed after seek"); + } catch (e) { + if (e.name != "InvalidStateError" || + e.code != DOMException.INVALID_STATE_ERR) + throw e; + ok(false, "Animation ended too early, even though begin time and " + + "simple duration didn't change"); + } + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilValues.xhtml b/dom/smil/test/test_smilValues.xhtml new file mode 100644 index 000000000..d2bce96a3 --- /dev/null +++ b/dom/smil/test/test_smilValues.xhtml @@ -0,0 +1,171 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL values</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=557885">Mozilla Bug + 474742</a> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL values **/ + +var gSvg = document.getElementById("svg"); +SimpleTest.waitForExplicitFinish(); + +function main() +{ + gSvg.pauseAnimations(); + + var testCases = Array(); + + // Single value + testCases.push({ + 'attr' : { 'values': 'a' }, + 'times': [ [ 0, 'a' ] ] + }); + + // The parsing below is based on the following discussion: + // + // http://lists.w3.org/Archives/Public/www-svg/2011Nov/0136.html + // + // In summary: + // * Values lists are semi-colon delimited and semi-colon terminated. + // * However, if there are extra non-whitespace characters after the final + // semi-colon then there's an implied semi-colon at the end. + // + // This differs to what is specified in SVG 1.1 but is consistent with the + // majority of browsers and with existing content (particularly that generated + // by Ikivo Animator). + + // Trailing semi-colon + testCases.push({ + 'attr' : { 'values': 'a;' }, + 'times': [ [ 0, 'a' ], [ 10, 'a' ] ] + }); + + // Trailing semi-colon + whitespace + testCases.push({ + 'attr' : { 'values': 'a; ' }, + 'times': [ [ 0, 'a' ], [ 10, 'a' ] ] + }); + + // Whitespace + trailing semi-colon + testCases.push({ + 'attr' : { 'values': 'a ;' }, + 'times': [ [ 0, 'a' ], [ 10, 'a' ] ] + }); + + // Empty at end + testCases.push({ + 'attr' : { 'values': 'a;;' }, + 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, '' ] ] + }); + + // Empty at end + whitespace + testCases.push({ + 'attr' : { 'values': 'a;; ' }, + 'times': [ [ 0, 'a' ], [ 4, 'a' ], [ 5, '' ], [ 10, '' ] ] + }); + + // Empty in middle + testCases.push({ + 'attr' : { 'values': 'a;;b' }, + 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, 'b' ] ] + }); + + // Empty in middle + trailing semi-colon + testCases.push({ + 'attr' : { 'values': 'a;;b;' }, + 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, 'b' ] ] + }); + + // Whitespace in middle + testCases.push({ + 'attr' : { 'values': 'a; ;b' }, + 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, 'b' ] ] + }); + + // Empty at start + testCases.push({ + 'attr' : { 'values': ';a' }, + 'times': [ [ 0, '' ], [ 5, 'a' ], [ 10, 'a' ] ] + }); + + // Whitespace at start + testCases.push({ + 'attr' : { 'values': ' ;a' }, + 'times': [ [ 0, '' ], [ 5, 'a' ], [ 10, 'a' ] ] + }); + + // Embedded whitespace + testCases.push({ + 'attr' : { 'values': ' a b ; c d ' }, + 'times': [ [ 0, 'a b' ], [ 5, 'c d' ], [ 10, 'c d' ] ] + }); + + // Whitespace only + testCases.push({ + 'attr' : { 'values': ' ' }, + 'times': [ [ 0, '' ], [ 10, '' ] ] + }); + + for (var i = 0; i < testCases.length; i++) { + gSvg.setCurrentTime(0); + var test = testCases[i]; + + // Create animation elements + var anim = createAnim(test.attr); + + // Run samples + for (var j = 0; j < test.times.length; j++) { + var curSample = test.times[j]; + gSvg.setCurrentTime(curSample[0]); + checkSample(anim, curSample[1], curSample[0], i); + } + + anim.parentNode.removeChild(anim); + } + + SimpleTest.finish(); +} + +function createAnim(attr) +{ + const svgns = "http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns, 'animate'); + anim.setAttribute('attributeName','class'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','0s'); + anim.setAttribute('fill','freeze'); + for (name in attr) { + anim.setAttribute(name, attr[name]); + } + return document.getElementById('circle').appendChild(anim); +} + +function checkSample(anim, expectedValue, sampleTime, caseNum) +{ + var msg = "Test case " + caseNum + + " (values: '" + anim.getAttribute('values') + "')," + + "t=" + sampleTime + + ": Unexpected sample value:"; + is(typeof anim.targetElement.className, "object"); + is(anim.targetElement.className.animVal, expectedValue, msg); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilXHR.xhtml b/dom/smil/test/test_smilXHR.xhtml new file mode 100644 index 000000000..d5202090e --- /dev/null +++ b/dom/smil/test/test_smilXHR.xhtml @@ -0,0 +1,88 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL Behavior in Data Documents</title> + <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529387">Mozilla Bug 529387</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL Behavior in Data Documents, with XMLHttpRequest **/ + +SimpleTest.waitForExplicitFinish(); + +function tryPausing(svg) { + // Check that pausing has no effect + ok(!svg.animationsPaused(), + "shouldn't be paused (because we shouldn't have even started"); + svg.pauseAnimations(); + ok(!svg.animationsPaused(), "attempts to pause should have no effect"); + svg.unpauseAnimations(); + ok(!svg.animationsPaused(), "still shouldn't be paused, after pause/unpause"); +} + +function trySeeking(svg) { + // Check that seeking is ineffective + is(svg.getCurrentTime(), 0, "should start out at time=0"); + svg.setCurrentTime(1); + is(svg.getCurrentTime(), 0, "shouldn't be able to seek away from time=0"); +} + +function tryBeginEnd(anim) { + // Check that beginning / ending a particular animation element will trigger + // exceptions. + var didThrow = false; + ok(anim, "need a non-null animate element"); + try { + anim.beginElement(); + } catch (e) { + didThrow = true; + } + ok(didThrow, "beginElement should fail"); + + didThrow = false; + try { + anim.endElement(); + } catch (e) { + didThrow = true; + } + ok(didThrow, "endElement should fail"); +} + +function main() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "smilXHR_helper.svg", false); + xhr.send(); + var xdoc = xhr.responseXML; + + var svg = xdoc.getElementById("svg"); + var circ = xdoc.getElementById("circ"); + var animXML = xdoc.getElementById("animXML"); + var animCSS = xdoc.getElementById("animCSS"); + + tryPausing(svg); + trySeeking(svg); + tryBeginEnd(animXML); + tryBeginEnd(animCSS); + + // Check that the actual values of our animated attr/prop aren't affected + is(circ.cx.animVal.value, circ.cx.baseVal.value, + "animation of attribute shouldn't be taking effect"); + is(SMILUtil.getComputedStyleSimple(circ, "opacity"), "1", + "animation of CSS property shouldn't be taking effect"); + + SimpleTest.finish(); +} + +window.addEventListener("load", main, false); +]]> +</script> +</pre> +</body> +</html> |