diff options
Diffstat (limited to '')
175 files changed, 21522 insertions, 0 deletions
diff --git a/dom/smil/SMILAnimationController.cpp b/dom/smil/SMILAnimationController.cpp new file mode 100644 index 0000000000..64c1054484 --- /dev/null +++ b/dom/smil/SMILAnimationController.cpp @@ -0,0 +1,724 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILAnimationController.h" + +#include <algorithm> + +#include "mozilla/AutoRestore.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresShellInlines.h" +#include "mozilla/RestyleManager.h" +#include "mozilla/SMILTimedElement.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsContentUtils.h" +#include "nsCSSProps.h" +#include "nsRefreshDriver.h" +#include "mozilla/dom/Document.h" +#include "SMILCompositor.h" +#include "SMILCSSProperty.h" + +using namespace mozilla::dom; + +namespace mozilla { + +//---------------------------------------------------------------------- +// SMILAnimationController implementation + +//---------------------------------------------------------------------- +// ctors, dtors, factory methods + +SMILAnimationController::SMILAnimationController(Document* aDoc) + : mAvgTimeBetweenSamples(0), + mResampleNeeded(false), + mDeferredStartSampling(false), + mRunningSample(false), + mRegisteredWithRefreshDriver(false), + mMightHavePendingStyleUpdates(false), + mDocument(aDoc) { + MOZ_ASSERT(aDoc, "need a non-null document"); + + nsRefreshDriver* refreshDriver = GetRefreshDriver(); + if (refreshDriver) { + mStartTime = refreshDriver->MostRecentRefresh(); + } else { + mStartTime = mozilla::TimeStamp::Now(); + } + mCurrentSampleTime = mStartTime; + + Begin(); +} + +SMILAnimationController::~SMILAnimationController() { + NS_ASSERTION(mAnimationElementTable.Count() == 0, + "Animation controller shouldn't be tracking any animation" + " elements when it dies"); + NS_ASSERTION(!mRegisteredWithRefreshDriver, + "Leaving stale entry in refresh driver's observer list"); +} + +void SMILAnimationController::Disconnect() { + MOZ_ASSERT(mDocument, "disconnecting when we weren't connected...?"); + MOZ_ASSERT(mRefCnt.get() == 1, + "Expecting to disconnect when doc is sole remaining owner"); + NS_ASSERTION(mPauseState & SMILTimeContainer::PAUSE_PAGEHIDE, + "Expecting to be paused for pagehide before disconnect"); + + StopSampling(GetRefreshDriver()); + + mDocument = nullptr; // (raw pointer) +} + +//---------------------------------------------------------------------- +// SMILTimeContainer methods: + +void SMILAnimationController::Pause(uint32_t aType) { + SMILTimeContainer::Pause(aType); + + if (mPauseState) { + mDeferredStartSampling = false; + StopSampling(GetRefreshDriver()); + } +} + +void SMILAnimationController::Resume(uint32_t aType) { + bool wasPaused = (mPauseState != 0); + // Update mCurrentSampleTime so that calls to GetParentTime--used for + // calculating parent offsets--are accurate + mCurrentSampleTime = mozilla::TimeStamp::Now(); + + SMILTimeContainer::Resume(aType); + + if (wasPaused && !mPauseState && !mChildContainerTable.IsEmpty()) { + MaybeStartSampling(GetRefreshDriver()); + Sample(); // Run the first sample manually + } +} + +SMILTime SMILAnimationController::GetParentTime() const { + return (SMILTime)(mCurrentSampleTime - mStartTime).ToMilliseconds(); +} + +//---------------------------------------------------------------------- +// nsARefreshObserver methods: +NS_IMPL_ADDREF(SMILAnimationController) +NS_IMPL_RELEASE(SMILAnimationController) + +// nsRefreshDriver Callback function +void SMILAnimationController::WillRefresh(mozilla::TimeStamp aTime) { + // Although we never expect aTime to go backwards, when we initialise the + // animation controller, if we can't get hold of a refresh driver we + // initialise mCurrentSampleTime to Now(). It may be possible that after + // doing so we get sampled by a refresh driver whose most recent refresh time + // predates when we were initialised, so to be safe we make sure to take the + // most recent time here. + aTime = std::max(mCurrentSampleTime, aTime); + + // Sleep detection: If the time between samples is a whole lot greater than we + // were expecting then we assume the computer went to sleep or someone's + // messing with the clock. In that case, fiddle our parent offset and use our + // average time between samples to calculate the new sample time. This + // prevents us from hanging while trying to catch up on all the missed time. + + // Smoothing of coefficient for the average function. 0.2 should let us track + // the sample rate reasonably tightly without being overly affected by + // occasional delays. + static const double SAMPLE_DUR_WEIGHTING = 0.2; + // If the elapsed time exceeds our expectation by this number of times we'll + // initiate special behaviour to basically ignore the intervening time. + static const double SAMPLE_DEV_THRESHOLD = 200.0; + + SMILTime elapsedTime = + (SMILTime)(aTime - mCurrentSampleTime).ToMilliseconds(); + if (mAvgTimeBetweenSamples == 0) { + // First sample. + mAvgTimeBetweenSamples = elapsedTime; + } else { + if (elapsedTime > SAMPLE_DEV_THRESHOLD * mAvgTimeBetweenSamples) { + // Unexpectedly long delay between samples. + NS_WARNING( + "Detected really long delay between samples, continuing from " + "previous sample"); + mParentOffset += elapsedTime - mAvgTimeBetweenSamples; + } + // Update the moving average. Due to truncation here the average will + // normally be a little less than it should be but that's probably ok. + mAvgTimeBetweenSamples = + (SMILTime)(elapsedTime * SAMPLE_DUR_WEIGHTING + + mAvgTimeBetweenSamples * (1.0 - SAMPLE_DUR_WEIGHTING)); + } + mCurrentSampleTime = aTime; + + Sample(); +} + +//---------------------------------------------------------------------- +// Animation element registration methods: + +void SMILAnimationController::RegisterAnimationElement( + SVGAnimationElement* aAnimationElement) { + mAnimationElementTable.PutEntry(aAnimationElement); + if (mDeferredStartSampling) { + mDeferredStartSampling = false; + if (!mChildContainerTable.IsEmpty()) { + // mAnimationElementTable was empty, but now we've added its 1st element + MOZ_ASSERT(mAnimationElementTable.Count() == 1, + "we shouldn't have deferred sampling if we already had " + "animations registered"); + StartSampling(GetRefreshDriver()); + Sample(); // Run the first sample manually + } // else, don't sample until a time container is registered (via AddChild) + } +} + +void SMILAnimationController::UnregisterAnimationElement( + SVGAnimationElement* aAnimationElement) { + mAnimationElementTable.RemoveEntry(aAnimationElement); +} + +//---------------------------------------------------------------------- +// Page show/hide + +void SMILAnimationController::OnPageShow() { + Resume(SMILTimeContainer::PAUSE_PAGEHIDE); +} + +void SMILAnimationController::OnPageHide() { + Pause(SMILTimeContainer::PAUSE_PAGEHIDE); +} + +//---------------------------------------------------------------------- +// Cycle-collection support + +void SMILAnimationController::Traverse( + nsCycleCollectionTraversalCallback* aCallback) { + // Traverse last compositor table + if (mLastCompositorTable) { + for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { + SMILCompositor* compositor = iter.Get(); + compositor->Traverse(aCallback); + } + } +} + +void SMILAnimationController::Unlink() { mLastCompositorTable = nullptr; } + +//---------------------------------------------------------------------- +// Refresh driver lifecycle related methods + +void SMILAnimationController::NotifyRefreshDriverCreated( + nsRefreshDriver* aRefreshDriver) { + if (!mPauseState && !mChildContainerTable.IsEmpty()) { + MaybeStartSampling(aRefreshDriver); + } +} + +void SMILAnimationController::NotifyRefreshDriverDestroying( + nsRefreshDriver* aRefreshDriver) { + if (!mPauseState && !mDeferredStartSampling) { + StopSampling(aRefreshDriver); + } +} + +//---------------------------------------------------------------------- +// Timer-related implementation helpers + +void SMILAnimationController::StartSampling(nsRefreshDriver* aRefreshDriver) { + NS_ASSERTION(mPauseState == 0, "Starting sampling but controller is paused"); + NS_ASSERTION(!mDeferredStartSampling, + "Started sampling but the deferred start flag is still set"); + if (aRefreshDriver) { + MOZ_ASSERT(!mRegisteredWithRefreshDriver, + "Redundantly registering with refresh driver"); + MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(), + "Starting sampling with wrong refresh driver"); + // We're effectively resuming from a pause so update our current sample time + // or else it will confuse our "average time between samples" calculations. + mCurrentSampleTime = mozilla::TimeStamp::Now(); + aRefreshDriver->AddRefreshObserver(this, FlushType::Style, + "SMIL animations"); + mRegisteredWithRefreshDriver = true; + } +} + +void SMILAnimationController::StopSampling(nsRefreshDriver* aRefreshDriver) { + if (aRefreshDriver && mRegisteredWithRefreshDriver) { + // NOTE: The document might already have been detached from its PresContext + // (and RefreshDriver), which would make GetRefreshDriver() return null. + MOZ_ASSERT(!GetRefreshDriver() || aRefreshDriver == GetRefreshDriver(), + "Stopping sampling with wrong refresh driver"); + aRefreshDriver->RemoveRefreshObserver(this, FlushType::Style); + mRegisteredWithRefreshDriver = false; + } +} + +void SMILAnimationController::MaybeStartSampling( + nsRefreshDriver* aRefreshDriver) { + if (mDeferredStartSampling) { + // We've received earlier 'MaybeStartSampling' calls, and we're + // deferring until we get a registered animation. + return; + } + + if (mAnimationElementTable.Count()) { + StartSampling(aRefreshDriver); + } else { + mDeferredStartSampling = true; + } +} + +//---------------------------------------------------------------------- +// Sample-related methods and callbacks + +void SMILAnimationController::DoSample() { + DoSample(true); // Skip unchanged time containers +} + +void SMILAnimationController::DoSample(bool aSkipUnchangedContainers) { + if (!mDocument) { + NS_ERROR("Shouldn't be sampling after document has disconnected"); + return; + } + if (mRunningSample) { + NS_ERROR("Shouldn't be recursively sampling"); + return; + } + + bool isStyleFlushNeeded = mResampleNeeded; + mResampleNeeded = false; + + nsCOMPtr<Document> document(mDocument); // keeps 'this' alive too + + // Set running sample flag -- do this before flushing styles so that when we + // flush styles we don't end up requesting extra samples + AutoRestore<bool> autoRestoreRunningSample(mRunningSample); + mRunningSample = true; + + // STEP 1: Bring model up to date + // (i) Rewind elements where necessary + // (ii) Run milestone samples + RewindElements(); + DoMilestoneSamples(); + + // STEP 2: Sample the child time containers + // + // When we sample the child time containers they will simply record the sample + // time in document time. + TimeContainerHashtable activeContainers(mChildContainerTable.Count()); + for (SMILTimeContainer* container : mChildContainerTable.Keys()) { + if (!container) { + continue; + } + + if (!container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN) && + (container->NeedsSample() || !aSkipUnchangedContainers)) { + container->ClearMilestones(); + container->Sample(); + container->MarkSeekFinished(); + activeContainers.PutEntry(container); + } + } + + // STEP 3: (i) Sample the timed elements AND + // (ii) Create a table of compositors + // + // (i) Here we sample the timed elements (fetched from the + // SVGAnimationElements) which determine from the active time if the + // element is active and what its simple time etc. is. This information is + // then passed to its time client (SMILAnimationFunction). + // + // (ii) During the same loop we also build up a table that contains one + // compositor for each animated attribute and which maps animated elements to + // the corresponding compositor for their target attribute. + // + // Note that this compositor table needs to be allocated on the heap so we can + // store it until the next sample. This lets us find out which elements were + // animated in sample 'n-1' but not in sample 'n' (and hence need to have + // their animation effects removed in sample 'n'). + // + // Parts (i) and (ii) are not functionally related but we combine them here to + // save iterating over the animation elements twice. + + // Create the compositor table + UniquePtr<SMILCompositorTable> currentCompositorTable( + new SMILCompositorTable(0)); + nsTArray<RefPtr<SVGAnimationElement>> animElems( + mAnimationElementTable.Count()); + + for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) { + SampleTimedElement(animElem, &activeContainers); + AddAnimationToCompositorTable(animElem, currentCompositorTable.get(), + isStyleFlushNeeded); + animElems.AppendElement(animElem); + } + activeContainers.Clear(); + + // STEP 4: Compare previous sample's compositors against this sample's. + // (Transfer cached base values across, & remove animation effects from + // no-longer-animated targets.) + if (mLastCompositorTable) { + // * Transfer over cached base values, from last sample's compositors + for (auto iter = currentCompositorTable->Iter(); !iter.Done(); + iter.Next()) { + SMILCompositor* compositor = iter.Get(); + SMILCompositor* lastCompositor = + mLastCompositorTable->GetEntry(compositor->GetKey()); + + if (lastCompositor) { + compositor->StealCachedBaseValue(lastCompositor); + } + } + + // * For each compositor in current sample's hash table, remove entry from + // prev sample's hash table -- we don't need to clear animation + // effects of those compositors, since they're still being animated. + for (const auto& key : currentCompositorTable->Keys()) { + mLastCompositorTable->RemoveEntry(key); + } + + // * For each entry that remains in prev sample's hash table (i.e. for + // every target that's no longer animated), clear animation effects. + for (auto iter = mLastCompositorTable->Iter(); !iter.Done(); iter.Next()) { + iter.Get()->ClearAnimationEffects(); + } + } + + // return early if there are no active animations to avoid a style flush + if (currentCompositorTable->Count() == 0) { + mLastCompositorTable = nullptr; + return; + } + + if (isStyleFlushNeeded) { + document->FlushPendingNotifications(FlushType::Style); + } + + // WARNING: + // WARNING: the above flush may have destroyed the pres shell and/or + // WARNING: frames and other layout related objects. + // WARNING: + + // STEP 5: Compose currently-animated attributes. + // XXXdholbert: This step traverses our animation targets in an effectively + // random order. For animation from/to 'inherit' values to work correctly + // when the inherited value is *also* being animated, we really should be + // traversing our animated nodes in an ancestors-first order (bug 501183) + bool mightHavePendingStyleUpdates = false; + for (auto iter = currentCompositorTable->Iter(); !iter.Done(); iter.Next()) { + iter.Get()->ComposeAttribute(mightHavePendingStyleUpdates); + } + + // Update last compositor table + mLastCompositorTable = std::move(currentCompositorTable); + mMightHavePendingStyleUpdates = mightHavePendingStyleUpdates; + + NS_ASSERTION(!mResampleNeeded, "Resample dirty flag set during sample!"); +} + +void SMILAnimationController::RewindElements() { + const bool rewindNeeded = std::any_of( + mChildContainerTable.Keys().cbegin(), mChildContainerTable.Keys().cend(), + [](SMILTimeContainer* container) { return container->NeedsRewind(); }); + + if (!rewindNeeded) return; + + for (SVGAnimationElement* animElem : mAnimationElementTable.Keys()) { + SMILTimeContainer* timeContainer = animElem->GetTimeContainer(); + if (timeContainer && timeContainer->NeedsRewind()) { + animElem->TimedElement().Rewind(); + } + } + + for (SMILTimeContainer* container : mChildContainerTable.Keys()) { + container->ClearNeedsRewind(); + } +} + +void SMILAnimationController::DoMilestoneSamples() { + // We need to sample the timing model but because SMIL operates independently + // of the frame-rate, we can get one sample at t=0s and the next at t=10min. + // + // In between those two sample times a whole string of significant events + // might be expected to take place: events firing, new interdependencies + // between animations resolved and dissolved, etc. + // + // Furthermore, at any given time, we want to sample all the intervals that + // end at that time BEFORE any that begin. This behaviour is implied by SMIL's + // endpoint-exclusive timing model. + // + // So we have the animations (specifically the timed elements) register the + // next significant moment (called a milestone) in their lifetime and then we + // step through the model at each of these moments and sample those animations + // registered for those times. This way events can fire in the correct order, + // dependencies can be resolved etc. + + SMILTime sampleTime = INT64_MIN; + + while (true) { + // We want to find any milestones AT OR BEFORE the current sample time so we + // initialise the next milestone to the moment after (1ms after, to be + // precise) the current sample time and see if there are any milestones + // before that. Any other milestones will be dealt with in a subsequent + // sample. + SMILMilestone nextMilestone(GetCurrentTimeAsSMILTime() + 1, true); + for (SMILTimeContainer* container : mChildContainerTable.Keys()) { + if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) { + continue; + } + SMILMilestone thisMilestone; + bool didGetMilestone = + container->GetNextMilestoneInParentTime(thisMilestone); + if (didGetMilestone && thisMilestone < nextMilestone) { + nextMilestone = thisMilestone; + } + } + + if (nextMilestone.mTime > GetCurrentTimeAsSMILTime()) { + break; + } + + nsTArray<RefPtr<mozilla::dom::SVGAnimationElement>> elements; + for (SMILTimeContainer* container : mChildContainerTable.Keys()) { + if (container->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) { + continue; + } + container->PopMilestoneElementsAtMilestone(nextMilestone, elements); + } + + uint32_t length = elements.Length(); + + // During the course of a sampling we don't want to actually go backwards. + // Due to negative offsets, early ends and the like, a timed element might + // register a milestone that is actually in the past. That's fine, but it's + // still only going to get *sampled* with whatever time we're up to and no + // earlier. + // + // Because we're only performing this clamping at the last moment, the + // animations will still all get sampled in the correct order and + // dependencies will be appropriately resolved. + sampleTime = std::max(nextMilestone.mTime, sampleTime); + + for (uint32_t i = 0; i < length; ++i) { + SVGAnimationElement* elem = elements[i].get(); + MOZ_ASSERT(elem, "nullptr animation element in list"); + SMILTimeContainer* container = elem->GetTimeContainer(); + if (!container) + // The container may be nullptr if the element has been detached from + // its parent since registering a milestone. + continue; + + SMILTimeValue containerTimeValue = + container->ParentToContainerTime(sampleTime); + if (!containerTimeValue.IsDefinite()) continue; + + // Clamp the converted container time to non-negative values. + SMILTime containerTime = + std::max<SMILTime>(0, containerTimeValue.GetMillis()); + + if (nextMilestone.mIsEnd) { + elem->TimedElement().SampleEndAt(containerTime); + } else { + elem->TimedElement().SampleAt(containerTime); + } + } + } +} + +/*static*/ +void SMILAnimationController::SampleTimedElement( + SVGAnimationElement* aElement, TimeContainerHashtable* aActiveContainers) { + SMILTimeContainer* timeContainer = aElement->GetTimeContainer(); + if (!timeContainer) return; + + // We'd like to call timeContainer->NeedsSample() here and skip all timed + // elements that belong to paused time containers that don't need a sample, + // but that doesn't work because we've already called Sample() on all the time + // containers so the paused ones don't need a sample any more and they'll + // return false. + // + // Instead we build up a hashmap of active time containers during the previous + // step (SampleTimeContainer) and then test here if the container for this + // timed element is in the list. + if (!aActiveContainers->GetEntry(timeContainer)) return; + + SMILTime containerTime = timeContainer->GetCurrentTimeAsSMILTime(); + + MOZ_ASSERT(!timeContainer->IsSeeking(), + "Doing a regular sample but the time container is still seeking"); + aElement->TimedElement().SampleAt(containerTime); +} + +/*static*/ +void SMILAnimationController::AddAnimationToCompositorTable( + SVGAnimationElement* aElement, SMILCompositorTable* aCompositorTable, + bool& aStyleFlushNeeded) { + // Add a compositor to the hash table if there's not already one there + SMILTargetIdentifier key; + if (!GetTargetIdentifierForAnimation(aElement, key)) + // Something's wrong/missing about animation's target; skip this animation + return; + + SMILAnimationFunction& func = aElement->AnimationFunction(); + + // Only add active animation functions. If there are no active animations + // targeting an attribute, no compositor will be created and any previously + // applied animations will be cleared. + if (func.IsActiveOrFrozen()) { + // Look up the compositor for our target, & add our animation function + // to its list of animation functions. + SMILCompositor* result = aCompositorTable->PutEntry(key); + result->AddAnimationFunction(&func); + + } else if (func.HasChanged()) { + // Look up the compositor for our target, and force it to skip the + // "nothing's changed so don't bother compositing" optimization for this + // sample. |func| is inactive, but it's probably *newly* inactive (since + // it's got HasChanged() == true), so we need to make sure to recompose + // its target. + SMILCompositor* result = aCompositorTable->PutEntry(key); + result->ToggleForceCompositing(); + + // We've now made sure that |func|'s inactivity will be reflected as of + // this sample. We need to clear its HasChanged() flag so that it won't + // trigger this same clause in future samples (until it changes again). + func.ClearHasChanged(); + } + aStyleFlushNeeded |= func.ValueNeedsReparsingEverySample(); +} + +static inline bool IsTransformAttribute(int32_t aNamespaceID, + nsAtom* aAttributeName) { + return aNamespaceID == kNameSpaceID_None && + (aAttributeName == nsGkAtoms::transform || + aAttributeName == nsGkAtoms::patternTransform || + aAttributeName == nsGkAtoms::gradientTransform); +} + +// Helper function that, given a SVGAnimationElement, looks up its target +// element & target attribute and populates a SMILTargetIdentifier +// for this target. +/*static*/ +bool SMILAnimationController::GetTargetIdentifierForAnimation( + SVGAnimationElement* aAnimElem, SMILTargetIdentifier& aResult) { + // Look up target (animated) element + Element* targetElem = aAnimElem->GetTargetElementContent(); + if (!targetElem) + // Animation has no target elem -- skip it. + return false; + + // Look up target (animated) attribute + // SMILANIM section 3.1, attributeName may + // have an XMLNS prefix to indicate the XML namespace. + RefPtr<nsAtom> attributeName; + int32_t attributeNamespaceID; + if (!aAnimElem->GetTargetAttributeName(&attributeNamespaceID, + getter_AddRefs(attributeName))) + // Animation has no target attr -- skip it. + return false; + + // animateTransform can only animate transforms, conversely transforms + // can only be animated by animateTransform + if (IsTransformAttribute(attributeNamespaceID, attributeName) != + (aAnimElem->IsSVGElement(nsGkAtoms::animateTransform))) + return false; + + // Construct the key + aResult.mElement = targetElem; + aResult.mAttributeName = attributeName; + aResult.mAttributeNamespaceID = attributeNamespaceID; + + return true; +} + +bool SMILAnimationController::PreTraverse() { + return PreTraverseInSubtree(nullptr); +} + +bool SMILAnimationController::PreTraverseInSubtree(Element* aRoot) { + MOZ_ASSERT(NS_IsMainThread()); + + if (!mMightHavePendingStyleUpdates) { + return false; + } + + nsPresContext* context = mDocument->GetPresContext(); + if (!context) { + return false; + } + + bool foundElementsNeedingRestyle = false; + for (SVGAnimationElement* animElement : mAnimationElementTable.Keys()) { + SMILTargetIdentifier key; + if (!GetTargetIdentifierForAnimation(animElement, key)) { + // Something's wrong/missing about animation's target; skip this animation + continue; + } + + // Ignore restyles that aren't in the flattened tree subtree rooted at + // aRoot. + if (aRoot && !nsContentUtils::ContentIsFlattenedTreeDescendantOf( + key.mElement, aRoot)) { + continue; + } + + context->RestyleManager()->PostRestyleEventForAnimations( + key.mElement, PseudoStyleType::NotPseudo, RestyleHint::RESTYLE_SMIL); + + foundElementsNeedingRestyle = true; + } + + // Only clear the mMightHavePendingStyleUpdates flag if we definitely posted + // all restyles. + if (!aRoot) { + mMightHavePendingStyleUpdates = false; + } + + return foundElementsNeedingRestyle; +} + +//---------------------------------------------------------------------- +// Add/remove child time containers + +nsresult SMILAnimationController::AddChild(SMILTimeContainer& aChild) { + TimeContainerPtrKey* key = mChildContainerTable.PutEntry(&aChild); + NS_ENSURE_TRUE(key, NS_ERROR_OUT_OF_MEMORY); + + if (!mPauseState && mChildContainerTable.Count() == 1) { + MaybeStartSampling(GetRefreshDriver()); + Sample(); // Run the first sample manually + } + + return NS_OK; +} + +void SMILAnimationController::RemoveChild(SMILTimeContainer& aChild) { + mChildContainerTable.RemoveEntry(&aChild); + + if (!mPauseState && mChildContainerTable.IsEmpty()) { + StopSampling(GetRefreshDriver()); + } +} + +// Helper method +nsRefreshDriver* SMILAnimationController::GetRefreshDriver() { + if (!mDocument) { + NS_ERROR("Requesting refresh driver after document has disconnected!"); + return nullptr; + } + + nsPresContext* context = mDocument->GetPresContext(); + return context ? context->RefreshDriver() : nullptr; +} + +void SMILAnimationController::FlagDocumentNeedsFlush() { + if (PresShell* presShell = mDocument->GetPresShell()) { + presShell->SetNeedStyleFlush(); + } +} + +} // namespace mozilla diff --git a/dom/smil/SMILAnimationController.h b/dom/smil/SMILAnimationController.h new file mode 100644 index 0000000000..6bf27e80e6 --- /dev/null +++ b/dom/smil/SMILAnimationController.h @@ -0,0 +1,210 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILANIMATIONCONTROLLER_H_ +#define DOM_SMIL_SMILANIMATIONCONTROLLER_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILCompositorTable.h" +#include "mozilla/SMILMilestone.h" +#include "mozilla/SMILTimeContainer.h" +#include "mozilla/UniquePtr.h" +#include "nsCOMPtr.h" +#include "nsTArray.h" +#include "nsTHashtable.h" +#include "nsHashKeys.h" +#include "nsRefreshObservers.h" + +class nsRefreshDriver; + +namespace mozilla { +struct SMILTargetIdentifier; +namespace dom { +class Element; +class SVGAnimationElement; +} // namespace dom + +//---------------------------------------------------------------------- +// SMILAnimationController +// +// The animation controller maintains the animation timer and determines the +// sample times and sample rate for all SMIL animations in a document. There is +// at most one animation controller per document so that frame-rate tuning can +// be performed at a document-level. +// +// The animation controller can contain many child time containers (timed +// document root objects) which may correspond to SVG document fragments within +// a compound document. These time containers can be paused individually or +// here, at the document level. +// +class SMILAnimationController final : public SMILTimeContainer, + public nsARefreshObserver { + public: + explicit SMILAnimationController(mozilla::dom::Document* aDoc); + + // Clears mDocument pointer. (Called by our mozilla::dom::Document when it's + // going away) + void Disconnect(); + + // SMILContainer + void Pause(uint32_t aType) override; + void Resume(uint32_t aType) override; + SMILTime GetParentTime() const override; + + // nsARefreshObserver + NS_IMETHOD_(MozExternalRefCountType) AddRef() override; + NS_IMETHOD_(MozExternalRefCountType) Release() override; + + void WillRefresh(mozilla::TimeStamp aTime) override; + + // Methods for registering and enumerating animation elements + void RegisterAnimationElement( + mozilla::dom::SVGAnimationElement* aAnimationElement); + void UnregisterAnimationElement( + mozilla::dom::SVGAnimationElement* aAnimationElement); + + // Methods for resampling all animations + // (A resample performs the same operations as a sample but doesn't advance + // the current time and doesn't check if the container is paused) + // This will flush pending style changes for the document. + void Resample() { DoSample(false); } + + void SetResampleNeeded() { + if (!mRunningSample && !mResampleNeeded) { + FlagDocumentNeedsFlush(); + mResampleNeeded = true; + } + } + + // This will flush pending style changes for the document. + void FlushResampleRequests() { + if (!mResampleNeeded) return; + + Resample(); + } + + // Methods for handling page transitions + void OnPageShow(); + void OnPageHide(); + + // Methods for supporting cycle-collection + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + // Methods for relaying the availability of the refresh driver + void NotifyRefreshDriverCreated(nsRefreshDriver* aRefreshDriver); + void NotifyRefreshDriverDestroying(nsRefreshDriver* aRefreshDriver); + + // Helper to check if we have any animation elements at all + bool HasRegisteredAnimations() const { + return mAnimationElementTable.Count() != 0; + } + + bool MightHavePendingStyleUpdates() const { + return mMightHavePendingStyleUpdates; + } + + bool PreTraverse(); + bool PreTraverseInSubtree(mozilla::dom::Element* aRoot); + + protected: + ~SMILAnimationController(); + + // alias declarations + using TimeContainerPtrKey = nsPtrHashKey<SMILTimeContainer>; + using TimeContainerHashtable = nsTHashtable<TimeContainerPtrKey>; + using AnimationElementPtrKey = nsPtrHashKey<dom::SVGAnimationElement>; + using AnimationElementHashtable = nsTHashtable<AnimationElementPtrKey>; + + // Returns mDocument's refresh driver, if it's got one. + nsRefreshDriver* GetRefreshDriver(); + + // Methods for controlling whether we're sampling + void StartSampling(nsRefreshDriver* aRefreshDriver); + void StopSampling(nsRefreshDriver* aRefreshDriver); + + // Wrapper for StartSampling that defers if no animations are registered. + void MaybeStartSampling(nsRefreshDriver* aRefreshDriver); + + // Sample-related callbacks and implementation helpers + void DoSample() override; + void DoSample(bool aSkipUnchangedContainers); + + void RewindElements(); + + void DoMilestoneSamples(); + + static void SampleTimedElement(mozilla::dom::SVGAnimationElement* aElement, + TimeContainerHashtable* aActiveContainers); + + static void AddAnimationToCompositorTable( + mozilla::dom::SVGAnimationElement* aElement, + SMILCompositorTable* aCompositorTable, bool& aStyleFlushNeeded); + + static bool GetTargetIdentifierForAnimation( + mozilla::dom::SVGAnimationElement* aAnimElem, + SMILTargetIdentifier& aResult); + + // Methods for adding/removing time containers + nsresult AddChild(SMILTimeContainer& aChild) override; + void RemoveChild(SMILTimeContainer& aChild) override; + + void FlagDocumentNeedsFlush(); + + // Members + nsAutoRefCnt mRefCnt; + NS_DECL_OWNINGTHREAD + + AnimationElementHashtable mAnimationElementTable; + TimeContainerHashtable mChildContainerTable; + mozilla::TimeStamp mCurrentSampleTime; + mozilla::TimeStamp mStartTime; + + // Average time between samples from the refresh driver. This is used to + // detect large unexpected gaps between samples such as can occur when the + // computer sleeps. The nature of the SMIL model means that catching up these + // large gaps can be expensive as, for example, many events may need to be + // dispatched for the intervening time when no samples were received. + // + // In such cases, we ignore the intervening gap and continue sampling from + // when we were expecting the next sample to arrive. + // + // Note that we only do this for SMIL and not CSS transitions (which doesn't + // have so much work to do to catch up) nor scripted animations (which expect + // animation time to follow real time). + // + // This behaviour does not affect pausing (since we're not *expecting* any + // samples then) nor seeking (where the SMIL model behaves somewhat + // differently such as not dispatching events). + SMILTime mAvgTimeBetweenSamples; + + bool mResampleNeeded; + // If we're told to start sampling but there are no animation elements we just + // record the time, set the following flag, and then wait until we have an + // animation element. Then we'll reset this flag and actually start sampling. + bool mDeferredStartSampling; + bool mRunningSample; + + // Are we registered with our document's refresh driver? + bool mRegisteredWithRefreshDriver; + + // Have we updated animated values without adding them to the restyle tracker? + bool mMightHavePendingStyleUpdates; + + // Store raw ptr to mDocument. It owns the controller, so controller + // shouldn't outlive it + mozilla::dom::Document* mDocument; + + // Contains compositors used in our last sample. We keep this around + // so we can detect when an element/attribute used to be animated, + // but isn't anymore for some reason. (e.g. if its <animate> element is + // removed or retargeted) + UniquePtr<SMILCompositorTable> mLastCompositorTable; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILANIMATIONCONTROLLER_H_ diff --git a/dom/smil/SMILAnimationFunction.cpp b/dom/smil/SMILAnimationFunction.cpp new file mode 100644 index 0000000000..43b470896e --- /dev/null +++ b/dom/smil/SMILAnimationFunction.cpp @@ -0,0 +1,978 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILAnimationFunction.h" + +#include <math.h> + +#include <algorithm> +#include <utility> + +#include "mozilla/DebugOnly.h" +#include "mozilla/SMILAttr.h" +#include "mozilla/SMILCSSValueType.h" +#include "mozilla/SMILNullType.h" +#include "mozilla/SMILParserUtils.h" +#include "mozilla/SMILTimedElement.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsAttrValueInlines.h" +#include "nsCOMArray.h" +#include "nsCOMPtr.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" +#include "nsIContent.h" +#include "nsReadableUtils.h" +#include "nsString.h" + +using namespace mozilla::dom; + +namespace mozilla { + +//---------------------------------------------------------------------- +// Static members + +nsAttrValue::EnumTable SMILAnimationFunction::sAccumulateTable[] = { + {"none", false}, {"sum", true}, {nullptr, 0}}; + +nsAttrValue::EnumTable SMILAnimationFunction::sAdditiveTable[] = { + {"replace", false}, {"sum", true}, {nullptr, 0}}; + +nsAttrValue::EnumTable SMILAnimationFunction::sCalcModeTable[] = { + {"linear", CALC_LINEAR}, + {"discrete", CALC_DISCRETE}, + {"paced", CALC_PACED}, + {"spline", CALC_SPLINE}, + {nullptr, 0}}; + +// Any negative number should be fine as a sentinel here, +// because valid distances are non-negative. +#define COMPUTE_DISTANCE_ERROR (-1) + +//---------------------------------------------------------------------- +// Constructors etc. + +SMILAnimationFunction::SMILAnimationFunction() + : mSampleTime(-1), + mRepeatIteration(0), + mBeginTime(INT64_MIN), + mAnimationElement(nullptr), + mErrorFlags(0), + mIsActive(false), + mIsFrozen(false), + mLastValue(false), + mHasChanged(true), + mValueNeedsReparsingEverySample(false), + mPrevSampleWasSingleValueAnimation(false), + mWasSkippedInPrevSample(false) {} + +void SMILAnimationFunction::SetAnimationElement( + SVGAnimationElement* aAnimationElement) { + mAnimationElement = aAnimationElement; +} + +bool SMILAnimationFunction::SetAttr(nsAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, + nsresult* aParseResult) { + bool foundMatch = true; + nsresult parseResult = NS_OK; + + // The attributes 'by', 'from', 'to', and 'values' may be parsed differently + // depending on the element & attribute we're animating. So instead of + // parsing them now we re-parse them at every sample. + if (aAttribute == nsGkAtoms::by || aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || aAttribute == nsGkAtoms::values) { + // We parse to, from, by, values at sample time. + // XXX Need to flag which attribute has changed and then when we parse it at + // sample time, report any errors and reset the flag + mHasChanged = true; + aResult.SetTo(aValue); + } else if (aAttribute == nsGkAtoms::accumulate) { + parseResult = SetAccumulate(aValue, aResult); + } else if (aAttribute == nsGkAtoms::additive) { + parseResult = SetAdditive(aValue, aResult); + } else if (aAttribute == nsGkAtoms::calcMode) { + parseResult = SetCalcMode(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keyTimes) { + parseResult = SetKeyTimes(aValue, aResult); + } else if (aAttribute == nsGkAtoms::keySplines) { + parseResult = SetKeySplines(aValue, aResult); + } else { + foundMatch = false; + } + + if (foundMatch && aParseResult) { + *aParseResult = parseResult; + } + + return foundMatch; +} + +bool SMILAnimationFunction::UnsetAttr(nsAtom* aAttribute) { + bool foundMatch = true; + + if (aAttribute == nsGkAtoms::by || aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::to || aAttribute == nsGkAtoms::values) { + mHasChanged = true; + } else if (aAttribute == nsGkAtoms::accumulate) { + UnsetAccumulate(); + } else if (aAttribute == nsGkAtoms::additive) { + UnsetAdditive(); + } else if (aAttribute == nsGkAtoms::calcMode) { + UnsetCalcMode(); + } else if (aAttribute == nsGkAtoms::keyTimes) { + UnsetKeyTimes(); + } else if (aAttribute == nsGkAtoms::keySplines) { + UnsetKeySplines(); + } else { + foundMatch = false; + } + + return foundMatch; +} + +void SMILAnimationFunction::SampleAt(SMILTime aSampleTime, + const SMILTimeValue& aSimpleDuration, + uint32_t aRepeatIteration) { + // * Update mHasChanged ("Might this sample be different from prev one?") + // Were we previously sampling a fill="freeze" final val? (We're not anymore.) + mHasChanged |= mLastValue; + + // Are we sampling at a new point in simple duration? And does that matter? + mHasChanged |= + (mSampleTime != aSampleTime || mSimpleDuration != aSimpleDuration) && + !IsValueFixedForSimpleDuration(); + + // Are we on a new repeat and accumulating across repeats? + if (!mErrorFlags) { // (can't call GetAccumulate() if we've had parse errors) + mHasChanged |= (mRepeatIteration != aRepeatIteration) && GetAccumulate(); + } + + mSampleTime = aSampleTime; + mSimpleDuration = aSimpleDuration; + mRepeatIteration = aRepeatIteration; + mLastValue = false; +} + +void SMILAnimationFunction::SampleLastValue(uint32_t aRepeatIteration) { + if (mHasChanged || !mLastValue || mRepeatIteration != aRepeatIteration) { + mHasChanged = true; + } + + mRepeatIteration = aRepeatIteration; + mLastValue = true; +} + +void SMILAnimationFunction::Activate(SMILTime aBeginTime) { + mBeginTime = aBeginTime; + mIsActive = true; + mIsFrozen = false; + mHasChanged = true; +} + +void SMILAnimationFunction::Inactivate(bool aIsFrozen) { + mIsActive = false; + mIsFrozen = aIsFrozen; + mHasChanged = true; +} + +void SMILAnimationFunction::ComposeResult(const SMILAttr& aSMILAttr, + SMILValue& aResult) { + mHasChanged = false; + mPrevSampleWasSingleValueAnimation = false; + mWasSkippedInPrevSample = false; + + // Skip animations that are inactive or in error + if (!IsActiveOrFrozen() || mErrorFlags != 0) return; + + // Get the animation values + SMILValueArray values; + nsresult rv = GetValues(aSMILAttr, values); + if (NS_FAILED(rv)) return; + + // Check that we have the right number of keySplines and keyTimes + CheckValueListDependentAttrs(values.Length()); + if (mErrorFlags != 0) return; + + // If this interval is active, we must have a non-negative mSampleTime + MOZ_ASSERT(mSampleTime >= 0 || !mIsActive, + "Negative sample time for active animation"); + MOZ_ASSERT(mSimpleDuration.IsResolved() || mLastValue, + "Unresolved simple duration for active or frozen animation"); + + // If we want to add but don't have a base value then just fail outright. + // This can happen when we skipped getting the base value because there's an + // animation function in the sandwich that should replace it but that function + // failed unexpectedly. + bool isAdditive = IsAdditive(); + if (isAdditive && aResult.IsNull()) return; + + SMILValue result; + + if (values.Length() == 1 && !IsToAnimation()) { + // Single-valued animation + result = values[0]; + mPrevSampleWasSingleValueAnimation = true; + + } else if (mLastValue) { + // Sampling last value + const SMILValue& last = values[values.Length() - 1]; + result = last; + + // See comment in AccumulateResult: to-animation does not accumulate + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + // If the target attribute type doesn't support addition Add will + // fail leaving result = last + result.Add(last, mRepeatIteration); + } + + } else { + // Interpolation + if (NS_FAILED(InterpolateResult(values, result, aResult))) return; + + if (NS_FAILED(AccumulateResult(values, result))) return; + } + + // If additive animation isn't required or isn't supported, set the value. + if (!isAdditive || NS_FAILED(aResult.SandwichAdd(result))) { + aResult = std::move(result); + } +} + +int8_t SMILAnimationFunction::CompareTo( + const SMILAnimationFunction* aOther) const { + NS_ENSURE_TRUE(aOther, 0); + + NS_ASSERTION(aOther != this, "Trying to compare to self"); + + // Inactive animations sort first + if (!IsActiveOrFrozen() && aOther->IsActiveOrFrozen()) return -1; + + if (IsActiveOrFrozen() && !aOther->IsActiveOrFrozen()) return 1; + + // Sort based on begin time + if (mBeginTime != aOther->GetBeginTime()) + return mBeginTime > aOther->GetBeginTime() ? 1 : -1; + + // Next sort based on syncbase dependencies: the dependent element sorts after + // its syncbase + const SMILTimedElement& thisTimedElement = mAnimationElement->TimedElement(); + const SMILTimedElement& otherTimedElement = + aOther->mAnimationElement->TimedElement(); + if (thisTimedElement.IsTimeDependent(otherTimedElement)) return 1; + if (otherTimedElement.IsTimeDependent(thisTimedElement)) return -1; + + // Animations that appear later in the document sort after those earlier in + // the document + MOZ_ASSERT(mAnimationElement != aOther->mAnimationElement, + "Two animations cannot have the same animation content element!"); + + return (nsContentUtils::PositionIsBefore(mAnimationElement, + aOther->mAnimationElement)) + ? -1 + : 1; +} + +bool SMILAnimationFunction::WillReplace() const { + /* + * In IsAdditive() we don't consider to-animation to be additive as it is + * a special case that is dealt with differently in the compositing method. + * Here, however, we return FALSE for to-animation (i.e. it will NOT replace + * the underlying value) as it builds on the underlying value. + */ + return !mErrorFlags && !(IsAdditive() || IsToAnimation()); +} + +bool SMILAnimationFunction::HasChanged() const { + return mHasChanged || mValueNeedsReparsingEverySample; +} + +bool SMILAnimationFunction::UpdateCachedTarget( + const SMILTargetIdentifier& aNewTarget) { + if (!mLastTarget.Equals(aNewTarget)) { + mLastTarget = aNewTarget; + return true; + } + return false; +} + +//---------------------------------------------------------------------- +// Implementation helpers + +nsresult SMILAnimationFunction::InterpolateResult(const SMILValueArray& aValues, + SMILValue& aResult, + SMILValue& aBaseValue) { + // Sanity check animation values + if ((!IsToAnimation() && aValues.Length() < 2) || + (IsToAnimation() && aValues.Length() != 1)) { + NS_ERROR("Unexpected number of values"); + return NS_ERROR_FAILURE; + } + + if (IsToAnimation() && aBaseValue.IsNull()) { + return NS_ERROR_FAILURE; + } + + // Get the normalised progress through the simple duration. + // + // If we have an indefinite simple duration, just set the progress to be + // 0 which will give us the expected behaviour of the animation being fixed at + // its starting point. + double simpleProgress = 0.0; + + if (mSimpleDuration.IsDefinite()) { + SMILTime dur = mSimpleDuration.GetMillis(); + + MOZ_ASSERT(dur >= 0, "Simple duration should not be negative"); + MOZ_ASSERT(mSampleTime >= 0, "Sample time should not be negative"); + + if (mSampleTime >= dur || mSampleTime < 0) { + NS_ERROR("Animation sampled outside interval"); + return NS_ERROR_FAILURE; + } + + if (dur > 0) { + simpleProgress = (double)mSampleTime / dur; + } // else leave simpleProgress at 0.0 (e.g. if mSampleTime == dur == 0) + } + + nsresult rv = NS_OK; + SMILCalcMode calcMode = GetCalcMode(); + + // Force discrete calcMode for visibility since StyleAnimationValue will + // try to interpolate it using the special clamping behavior defined for + // CSS. + if (SMILCSSValueType::PropertyFromValue(aValues[0]) == + eCSSProperty_visibility) { + calcMode = CALC_DISCRETE; + } + + if (calcMode != CALC_DISCRETE) { + // Get the normalised progress between adjacent values + const SMILValue* from = nullptr; + const SMILValue* to = nullptr; + // Init to -1 to make sure that if we ever forget to set this, the + // MOZ_ASSERT that tests that intervalProgress is in range will fail. + double intervalProgress = -1.f; + if (IsToAnimation()) { + from = &aBaseValue; + to = &aValues[0]; + if (calcMode == CALC_PACED) { + // Note: key[Times/Splines/Points] are ignored for calcMode="paced" + intervalProgress = simpleProgress; + } else { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + intervalProgress = ScaleIntervalProgress(scaledSimpleProgress, 0); + } + } else if (calcMode == CALC_PACED) { + rv = ComputePacedPosition(aValues, simpleProgress, intervalProgress, from, + to); + // Note: If the above call fails, we'll skip the "from->Interpolate" + // call below, and we'll drop into the CALC_DISCRETE section + // instead. (as the spec says we should, because our failure was + // presumably due to the values being non-additive) + } else { // calcMode == CALC_LINEAR or calcMode == CALC_SPLINE + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, calcMode); + uint32_t index = + (uint32_t)floor(scaledSimpleProgress * (aValues.Length() - 1)); + from = &aValues[index]; + to = &aValues[index + 1]; + intervalProgress = scaledSimpleProgress * (aValues.Length() - 1) - index; + intervalProgress = ScaleIntervalProgress(intervalProgress, index); + } + + if (NS_SUCCEEDED(rv)) { + MOZ_ASSERT(from, "NULL from-value during interpolation"); + MOZ_ASSERT(to, "NULL to-value during interpolation"); + MOZ_ASSERT(0.0f <= intervalProgress && intervalProgress < 1.0f, + "Interval progress should be in the range [0, 1)"); + rv = from->Interpolate(*to, intervalProgress, aResult); + } + } + + // Discrete-CalcMode case + // Note: If interpolation failed (isn't supported for this type), the SVG + // spec says to force discrete mode. + if (calcMode == CALC_DISCRETE || NS_FAILED(rv)) { + double scaledSimpleProgress = + ScaleSimpleProgress(simpleProgress, CALC_DISCRETE); + + // Floating-point errors can mean that, for example, a sample time of 29s in + // a 100s duration animation gives us a simple progress of 0.28999999999 + // instead of the 0.29 we'd expect. Normally this isn't a noticeable + // problem, but when we have sudden jumps in animation values (such as is + // the case here with discrete animation) we can get unexpected results. + // + // To counteract this, before we perform a floor() on the animation + // progress, we add a tiny fudge factor to push us into the correct interval + // in cases where floating-point errors might cause us to fall short. + static const double kFloatingPointFudgeFactor = 1.0e-16; + if (scaledSimpleProgress + kFloatingPointFudgeFactor <= 1.0) { + scaledSimpleProgress += kFloatingPointFudgeFactor; + } + + if (IsToAnimation()) { + // We don't follow SMIL 3, 12.6.4, where discrete to animations + // are the same as <set> animations. Instead, we treat it as a + // discrete animation with two values (the underlying value and + // the to="" value), and honor keyTimes="" as well. + uint32_t index = (uint32_t)floor(scaledSimpleProgress * 2); + aResult = index == 0 ? aBaseValue : aValues[0]; + } else { + uint32_t index = (uint32_t)floor(scaledSimpleProgress * aValues.Length()); + aResult = aValues[index]; + + // For animation of CSS properties, normally when interpolating we perform + // a zero-value fixup which means that empty values (values with type + // SMILCSSValueType but a null pointer value) are converted into + // a suitable zero value based on whatever they're being interpolated + // with. For discrete animation, however, since we don't interpolate, + // that never happens. In some rare cases, such as discrete non-additive + // by-animation, we can arrive here with |aResult| being such an empty + // value so we need to manually perform the fixup. + // + // We could define a generic method for this on SMILValue but its faster + // and simpler to just special case SMILCSSValueType. + if (aResult.mType == &SMILCSSValueType::sSingleton) { + // We have currently only ever encountered this case for the first + // value of a by-animation (which has two values) and since we have no + // way of testing other cases we just skip them (but assert if we + // ever do encounter them so that we can add code to handle them). + if (index + 1 >= aValues.Length()) { + MOZ_ASSERT(aResult.mU.mPtr, "The last value should not be empty"); + } else { + // Base the type of the zero value on the next element in the series. + SMILCSSValueType::FinalizeValue(aResult, aValues[index + 1]); + } + } + } + rv = NS_OK; + } + return rv; +} + +nsresult SMILAnimationFunction::AccumulateResult(const SMILValueArray& aValues, + SMILValue& aResult) { + if (!IsToAnimation() && GetAccumulate() && mRepeatIteration) { + const SMILValue& lastValue = aValues[aValues.Length() - 1]; + + // If the target attribute type doesn't support addition, Add will + // fail and we leave aResult untouched. + aResult.Add(lastValue, mRepeatIteration); + } + + return NS_OK; +} + +/* + * Given the simple progress for a paced animation, this method: + * - determines which two elements of the values array we're in between + * (returned as aFrom and aTo) + * - determines where we are between them + * (returned as aIntervalProgress) + * + * Returns NS_OK, or NS_ERROR_FAILURE if our values don't support distance + * computation. + */ +nsresult SMILAnimationFunction::ComputePacedPosition( + const SMILValueArray& aValues, double aSimpleProgress, + double& aIntervalProgress, const SMILValue*& aFrom, const SMILValue*& aTo) { + NS_ASSERTION(0.0f <= aSimpleProgress && aSimpleProgress < 1.0f, + "aSimpleProgress is out of bounds"); + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + MOZ_ASSERT(aValues.Length() >= 2, "Unexpected number of values"); + + // Trivial case: If we have just 2 values, then there's only one interval + // for us to traverse, and our progress across that interval is the exact + // same as our overall progress. + if (aValues.Length() == 2) { + aIntervalProgress = aSimpleProgress; + aFrom = &aValues[0]; + aTo = &aValues[1]; + return NS_OK; + } + + double totalDistance = ComputePacedTotalDistance(aValues); + if (totalDistance == COMPUTE_DISTANCE_ERROR) return NS_ERROR_FAILURE; + + // If we have 0 total distance, then it's unclear where our "paced" position + // should be. We can just fail, which drops us into discrete animation mode. + // (That's fine, since our values are apparently indistinguishable anyway.) + if (totalDistance == 0.0) { + return NS_ERROR_FAILURE; + } + + // total distance we should have moved at this point in time. + // (called 'remainingDist' due to how it's used in loop below) + double remainingDist = aSimpleProgress * totalDistance; + + // Must be satisfied, because totalDistance is a sum of (non-negative) + // distances, and aSimpleProgress is non-negative + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + // Find where remainingDist puts us in the list of values + // Note: We could optimize this next loop by caching the + // interval-distances in an array, but maybe that's excessive. + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + // Note: The following assertion is valid because remainingDist should + // start out non-negative, and this loop never shaves off more than its + // current value. + NS_ASSERTION(remainingDist >= 0, "distance values must be non-negative"); + + double curIntervalDist; + + DebugOnly<nsresult> rv = + aValues[i].ComputeDistance(aValues[i + 1], curIntervalDist); + MOZ_ASSERT(NS_SUCCEEDED(rv), + "If we got through ComputePacedTotalDistance, we should " + "be able to recompute each sub-distance without errors"); + + NS_ASSERTION(curIntervalDist >= 0, "distance values must be non-negative"); + // Clamp distance value at 0, just in case ComputeDistance is evil. + curIntervalDist = std::max(curIntervalDist, 0.0); + + if (remainingDist >= curIntervalDist) { + remainingDist -= curIntervalDist; + } else { + // NOTE: If we get here, then curIntervalDist necessarily is not 0. Why? + // Because this clause is only hit when remainingDist < curIntervalDist, + // and if curIntervalDist were 0, that would mean remainingDist would + // have to be < 0. But that can't happen, because remainingDist (as + // a distance) is non-negative by definition. + NS_ASSERTION(curIntervalDist != 0, + "We should never get here with this set to 0..."); + + // We found the right spot -- an interpolated position between + // values i and i+1. + aFrom = &aValues[i]; + aTo = &aValues[i + 1]; + aIntervalProgress = remainingDist / curIntervalDist; + return NS_OK; + } + } + + MOZ_ASSERT_UNREACHABLE( + "shouldn't complete loop & get here -- if we do, " + "then aSimpleProgress was probably out of bounds"); + return NS_ERROR_FAILURE; +} + +/* + * Computes the total distance to be travelled by a paced animation. + * + * Returns the total distance, or returns COMPUTE_DISTANCE_ERROR if + * our values don't support distance computation. + */ +double SMILAnimationFunction::ComputePacedTotalDistance( + const SMILValueArray& aValues) const { + NS_ASSERTION(GetCalcMode() == CALC_PACED, + "Calling paced-specific function, but not in paced mode"); + + double totalDistance = 0.0; + for (uint32_t i = 0; i < aValues.Length() - 1; i++) { + double tmpDist; + nsresult rv = aValues[i].ComputeDistance(aValues[i + 1], tmpDist); + if (NS_FAILED(rv)) { + return COMPUTE_DISTANCE_ERROR; + } + + // Clamp distance value to 0, just in case we have an evil ComputeDistance + // implementation somewhere + MOZ_ASSERT(tmpDist >= 0.0f, "distance values must be non-negative"); + tmpDist = std::max(tmpDist, 0.0); + + totalDistance += tmpDist; + } + + return totalDistance; +} + +double SMILAnimationFunction::ScaleSimpleProgress(double aProgress, + SMILCalcMode aCalcMode) { + if (!HasAttr(nsGkAtoms::keyTimes)) return aProgress; + + uint32_t numTimes = mKeyTimes.Length(); + + if (numTimes < 2) return aProgress; + + uint32_t i = 0; + for (; i < numTimes - 2 && aProgress >= mKeyTimes[i + 1]; ++i) { + } + + if (aCalcMode == CALC_DISCRETE) { + // discrete calcMode behaviour differs in that each keyTime defines the time + // from when the corresponding value is set, and therefore the last value + // needn't be 1. So check if we're in the last 'interval', that is, the + // space between the final value and 1.0. + if (aProgress >= mKeyTimes[i + 1]) { + MOZ_ASSERT(i == numTimes - 2, + "aProgress is not in range of the current interval, yet the " + "current interval is not the last bounded interval either."); + ++i; + } + return (double)i / numTimes; + } + + double& intervalStart = mKeyTimes[i]; + double& intervalEnd = mKeyTimes[i + 1]; + + double intervalLength = intervalEnd - intervalStart; + if (intervalLength <= 0.0) return intervalStart; + + return (i + (aProgress - intervalStart) / intervalLength) / + double(numTimes - 1); +} + +double SMILAnimationFunction::ScaleIntervalProgress(double aProgress, + uint32_t aIntervalIndex) { + if (GetCalcMode() != CALC_SPLINE) return aProgress; + + if (!HasAttr(nsGkAtoms::keySplines)) return aProgress; + + MOZ_ASSERT(aIntervalIndex < mKeySplines.Length(), "Invalid interval index"); + + SMILKeySpline const& spline = mKeySplines[aIntervalIndex]; + return spline.GetSplineValue(aProgress); +} + +bool SMILAnimationFunction::HasAttr(nsAtom* aAttName) const { + return mAnimationElement->HasAttr(aAttName); +} + +const nsAttrValue* SMILAnimationFunction::GetAttr(nsAtom* aAttName) const { + return mAnimationElement->GetParsedAttr(aAttName); +} + +bool SMILAnimationFunction::GetAttr(nsAtom* aAttName, + nsAString& aResult) const { + return mAnimationElement->GetAttr(aAttName, aResult); +} + +/* + * A utility function to make querying an attribute that corresponds to an + * SMILValue a little neater. + * + * @param aAttName The attribute name (in the global namespace). + * @param aSMILAttr The SMIL attribute to perform the parsing. + * @param[out] aResult The resulting SMILValue. + * @param[out] aPreventCachingOfSandwich + * If |aResult| contains dependencies on its context that + * should prevent the result of the animation sandwich from + * being cached and reused in future samples (as reported + * by SMILAttr::ValueFromString), then this outparam + * will be set to true. Otherwise it is left unmodified. + * + * Returns false if a parse error occurred, otherwise returns true. + */ +bool SMILAnimationFunction::ParseAttr(nsAtom* aAttName, + const SMILAttr& aSMILAttr, + SMILValue& aResult, + bool& aPreventCachingOfSandwich) const { + nsAutoString attValue; + if (GetAttr(aAttName, attValue)) { + bool preventCachingOfSandwich = false; + nsresult rv = aSMILAttr.ValueFromString(attValue, mAnimationElement, + aResult, preventCachingOfSandwich); + if (NS_FAILED(rv)) return false; + + if (preventCachingOfSandwich) { + aPreventCachingOfSandwich = true; + } + } + return true; +} + +/* + * SMILANIM specifies the following rules for animation function values: + * + * (1) if values is set, it overrides everything + * (2) for from/to/by animation at least to or by must be specified, from on its + * own (or nothing) is an error--which we will ignore + * (3) if both by and to are specified only to will be used, by will be ignored + * (4) if by is specified without from (by animation), forces additive behaviour + * (5) if to is specified without from (to animation), special care needs to be + * taken when compositing animation as such animations are composited last. + * + * This helper method applies these rules to fill in the values list and to set + * some internal state. + */ +nsresult SMILAnimationFunction::GetValues(const SMILAttr& aSMILAttr, + SMILValueArray& aResult) { + if (!mAnimationElement) return NS_ERROR_FAILURE; + + mValueNeedsReparsingEverySample = false; + SMILValueArray result; + + // If "values" is set, use it + if (HasAttr(nsGkAtoms::values)) { + nsAutoString attValue; + GetAttr(nsGkAtoms::values, attValue); + bool preventCachingOfSandwich = false; + if (!SMILParserUtils::ParseValues(attValue, mAnimationElement, aSMILAttr, + result, preventCachingOfSandwich)) { + return NS_ERROR_FAILURE; + } + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + // Else try to/from/by + } else { + bool preventCachingOfSandwich = false; + bool parseOk = true; + SMILValue to, from, by; + parseOk &= + ParseAttr(nsGkAtoms::to, aSMILAttr, to, preventCachingOfSandwich); + parseOk &= + ParseAttr(nsGkAtoms::from, aSMILAttr, from, preventCachingOfSandwich); + parseOk &= + ParseAttr(nsGkAtoms::by, aSMILAttr, by, preventCachingOfSandwich); + + if (preventCachingOfSandwich) { + mValueNeedsReparsingEverySample = true; + } + + if (!parseOk || !result.SetCapacity(2, fallible)) { + return NS_ERROR_FAILURE; + } + + // AppendElement() below must succeed, because SetCapacity() succeeded. + if (!to.IsNull()) { + if (!from.IsNull()) { + MOZ_ALWAYS_TRUE(result.AppendElement(from, fallible)); + MOZ_ALWAYS_TRUE(result.AppendElement(to, fallible)); + } else { + MOZ_ALWAYS_TRUE(result.AppendElement(to, fallible)); + } + } else if (!by.IsNull()) { + SMILValue effectiveFrom(by.mType); + if (!from.IsNull()) effectiveFrom = from; + // Set values to 'from; from + by' + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveFrom, fallible)); + SMILValue effectiveTo(effectiveFrom); + if (!effectiveTo.IsNull() && NS_SUCCEEDED(effectiveTo.Add(by))) { + MOZ_ALWAYS_TRUE(result.AppendElement(effectiveTo, fallible)); + } else { + // Using by-animation with non-additive type or bad base-value + return NS_ERROR_FAILURE; + } + } else { + // No values, no to, no by -- call it a day + return NS_ERROR_FAILURE; + } + } + + aResult = std::move(result); + + return NS_OK; +} + +void SMILAnimationFunction::CheckValueListDependentAttrs(uint32_t aNumValues) { + CheckKeyTimes(aNumValues); + CheckKeySplines(aNumValues); +} + +/** + * Performs checks for the keyTimes attribute required by the SMIL spec but + * which depend on other attributes and therefore needs to be updated as + * dependent attributes are set. + */ +void SMILAnimationFunction::CheckKeyTimes(uint32_t aNumValues) { + if (!HasAttr(nsGkAtoms::keyTimes)) return; + + SMILCalcMode calcMode = GetCalcMode(); + + // attribute is ignored for calcMode = paced + if (calcMode == CALC_PACED) { + SetKeyTimesErrorFlag(false); + return; + } + + uint32_t numKeyTimes = mKeyTimes.Length(); + if (numKeyTimes < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeyTimesErrorFlag(true); + return; + } + + // no. keyTimes == no. values + // For to-animation the number of values is considered to be 2. + bool matchingNumOfValues = numKeyTimes == (IsToAnimation() ? 2 : aNumValues); + if (!matchingNumOfValues) { + SetKeyTimesErrorFlag(true); + return; + } + + // first value must be 0 + if (mKeyTimes[0] != 0.0) { + SetKeyTimesErrorFlag(true); + return; + } + + // last value must be 1 for linear or spline calcModes + if (calcMode != CALC_DISCRETE && numKeyTimes > 1 && + mKeyTimes[numKeyTimes - 1] != 1.0) { + SetKeyTimesErrorFlag(true); + return; + } + + SetKeyTimesErrorFlag(false); +} + +void SMILAnimationFunction::CheckKeySplines(uint32_t aNumValues) { + // attribute is ignored if calc mode is not spline + if (GetCalcMode() != CALC_SPLINE) { + SetKeySplinesErrorFlag(false); + return; + } + + // calc mode is spline but the attribute is not set + if (!HasAttr(nsGkAtoms::keySplines)) { + SetKeySplinesErrorFlag(false); + return; + } + + if (mKeySplines.Length() < 1) { + // keyTimes isn't set or failed preliminary checks + SetKeySplinesErrorFlag(true); + return; + } + + // ignore splines if there's only one value + if (aNumValues == 1 && !IsToAnimation()) { + SetKeySplinesErrorFlag(false); + return; + } + + // no. keySpline specs == no. values - 1 + uint32_t splineSpecs = mKeySplines.Length(); + if ((splineSpecs != aNumValues - 1 && !IsToAnimation()) || + (IsToAnimation() && splineSpecs != 1)) { + SetKeySplinesErrorFlag(true); + return; + } + + SetKeySplinesErrorFlag(false); +} + +bool SMILAnimationFunction::IsValueFixedForSimpleDuration() const { + return mSimpleDuration.IsIndefinite() || + (!mHasChanged && mPrevSampleWasSingleValueAnimation); +} + +//---------------------------------------------------------------------- +// Property getters + +bool SMILAnimationFunction::GetAccumulate() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::accumulate); + if (!value) return false; + + return value->GetEnumValue(); +} + +bool SMILAnimationFunction::GetAdditive() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::additive); + if (!value) return false; + + return value->GetEnumValue(); +} + +SMILAnimationFunction::SMILCalcMode SMILAnimationFunction::GetCalcMode() const { + const nsAttrValue* value = GetAttr(nsGkAtoms::calcMode); + if (!value) return CALC_LINEAR; + + return SMILCalcMode(value->GetEnumValue()); +} + +//---------------------------------------------------------------------- +// Property setters / un-setters: + +nsresult SMILAnimationFunction::SetAccumulate(const nsAString& aAccumulate, + nsAttrValue& aResult) { + mHasChanged = true; + bool parseResult = + aResult.ParseEnumValue(aAccumulate, sAccumulateTable, true); + SetAccumulateErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILAnimationFunction::UnsetAccumulate() { + SetAccumulateErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetAdditive(const nsAString& aAdditive, + nsAttrValue& aResult) { + mHasChanged = true; + bool parseResult = aResult.ParseEnumValue(aAdditive, sAdditiveTable, true); + SetAdditiveErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILAnimationFunction::UnsetAdditive() { + SetAdditiveErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetCalcMode(const nsAString& aCalcMode, + nsAttrValue& aResult) { + mHasChanged = true; + bool parseResult = aResult.ParseEnumValue(aCalcMode, sCalcModeTable, true); + SetCalcModeErrorFlag(!parseResult); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILAnimationFunction::UnsetCalcMode() { + SetCalcModeErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetKeySplines(const nsAString& aKeySplines, + nsAttrValue& aResult) { + mKeySplines.Clear(); + aResult.SetTo(aKeySplines); + + mHasChanged = true; + + if (!SMILParserUtils::ParseKeySplines(aKeySplines, mKeySplines)) { + mKeySplines.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void SMILAnimationFunction::UnsetKeySplines() { + mKeySplines.Clear(); + SetKeySplinesErrorFlag(false); + mHasChanged = true; +} + +nsresult SMILAnimationFunction::SetKeyTimes(const nsAString& aKeyTimes, + nsAttrValue& aResult) { + mKeyTimes.Clear(); + aResult.SetTo(aKeyTimes); + + mHasChanged = true; + + if (!SMILParserUtils::ParseSemicolonDelimitedProgressList(aKeyTimes, true, + mKeyTimes)) { + mKeyTimes.Clear(); + return NS_ERROR_FAILURE; + } + + return NS_OK; +} + +void SMILAnimationFunction::UnsetKeyTimes() { + mKeyTimes.Clear(); + SetKeyTimesErrorFlag(false); + mHasChanged = true; +} + +} // namespace mozilla diff --git a/dom/smil/SMILAnimationFunction.h b/dom/smil/SMILAnimationFunction.h new file mode 100644 index 0000000000..9a12f1a846 --- /dev/null +++ b/dom/smil/SMILAnimationFunction.h @@ -0,0 +1,442 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILANIMATIONFUNCTION_H_ +#define DOM_SMIL_SMILANIMATIONFUNCTION_H_ + +#include "mozilla/SMILAttr.h" +#include "mozilla/SMILKeySpline.h" +#include "mozilla/SMILTargetIdentifier.h" +#include "mozilla/SMILTimeValue.h" +#include "mozilla/SMILTypes.h" +#include "mozilla/SMILValue.h" +#include "nsAttrValue.h" +#include "nsGkAtoms.h" +#include "nsString.h" +#include "nsTArray.h" + +namespace mozilla { +namespace dom { +class SVGAnimationElement; +} // namespace dom + +//---------------------------------------------------------------------- +// SMILAnimationFunction +// +// The animation function calculates animation values. It it is provided with +// time parameters (sample time, repeat iteration etc.) and it uses this to +// build an appropriate animation value by performing interpolation and +// addition operations. +// +// It is responsible for implementing the animation parameters of an animation +// element (e.g. from, by, to, values, calcMode, additive, accumulate, keyTimes, +// keySplines) +// +class SMILAnimationFunction { + public: + SMILAnimationFunction(); + + /* + * Sets the owning animation element which this class uses to query attribute + * values and compare document positions. + */ + void SetAnimationElement( + mozilla::dom::SVGAnimationElement* aAnimationElement); + + /* + * Sets animation-specific attributes (or marks them dirty, in the case + * of from/to/by/values). + * + * @param aAttribute The attribute being set + * @param aValue The updated value of the attribute. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aParseResult Outparam used for reporting parse errors. Will be set + * to NS_OK if everything succeeds. + * @return true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool SetAttr(nsAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, nsresult* aParseResult = nullptr); + + /* + * Unsets the given attribute. + * + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + virtual bool UnsetAttr(nsAtom* aAttribute); + + /** + * Indicate a new sample has occurred. + * + * @param aSampleTime The sample time for this timed element expressed in + * simple time. + * @param aSimpleDuration The simple duration for this timed element. + * @param aRepeatIteration The repeat iteration for this sample. The first + * iteration has a value of 0. + */ + void SampleAt(SMILTime aSampleTime, const SMILTimeValue& aSimpleDuration, + uint32_t aRepeatIteration); + + /** + * Indicate to sample using the last value defined for the animation function. + * This value is not normally sampled due to the end-point exclusive timing + * model but only occurs when the fill mode is "freeze" and the active + * duration is an even multiple of the simple duration. + * + * @param aRepeatIteration The repeat iteration for this sample. The first + * iteration has a value of 0. + */ + void SampleLastValue(uint32_t aRepeatIteration); + + /** + * Indicate that this animation is now active. This is used to instruct the + * animation function that it should now add its result to the animation + * sandwich. The begin time is also provided for proper prioritization of + * animation functions, and for this reason, this method must be called + * before either of the Sample methods. + * + * @param aBeginTime The begin time for the newly active interval. + */ + void Activate(SMILTime aBeginTime); + + /** + * Indicate that this animation is no longer active. This is used to instruct + * the animation function that it should no longer add its result to the + * animation sandwich. + * + * @param aIsFrozen true if this animation should continue to contribute + * to the animation sandwich using the most recent sample + * parameters. + */ + void Inactivate(bool aIsFrozen); + + /** + * Combines the result of this animation function for the last sample with the + * specified value. + * + * @param aSMILAttr This animation's target attribute. Used here for + * doing attribute-specific parsing of from/to/by/values. + * + * @param aResult The value to compose with. + */ + void ComposeResult(const SMILAttr& aSMILAttr, SMILValue& aResult); + + /** + * Returns the relative priority of this animation to another. The priority is + * used for determining the position of the animation in the animation + * sandwich -- higher priority animations are applied on top of lower + * priority animations. + * + * @return -1 if this animation has lower priority or 1 if this animation has + * higher priority + * + * This method should never return any other value, including 0. + */ + int8_t CompareTo(const SMILAnimationFunction* aOther) const; + + /* + * The following methods are provided so that the compositor can optimize its + * operations by only composing those animation that will affect the final + * result. + */ + + /** + * Indicates if the animation is currently active or frozen. Inactive + * animations will not contribute to the composed result. + * + * @return true if the animation is active or frozen, false otherwise. + */ + bool IsActiveOrFrozen() const { + /* + * - Frozen animations should be considered active for the purposes of + * compositing. + * - This function does not assume that our SMILValues (by/from/to/values) + * have already been parsed. + */ + return (mIsActive || mIsFrozen); + } + + /** + * Indicates if the animation is active. + * + * @return true if the animation is active, false otherwise. + */ + bool IsActive() const { return mIsActive; } + + /** + * Indicates if this animation will replace the passed in result rather than + * adding to it. Animations that replace the underlying value may be called + * without first calling lower priority animations. + * + * @return True if the animation will replace, false if it will add or + * otherwise build on the passed in value. + */ + virtual bool WillReplace() const; + + /** + * Indicates if the parameters for this animation have changed since the last + * time it was composited. This allows rendering to be performed only when + * necessary, particularly when no animations are active. + * + * Note that the caller is responsible for determining if the animation + * target has changed (with help from my UpdateCachedTarget() method). + * + * @return true if the animation parameters have changed, false + * otherwise. + */ + bool HasChanged() const; + + /** + * This method lets us clear the 'HasChanged' flag for inactive animations + * after we've reacted to their change to the 'inactive' state, so that we + * won't needlessly recompose their targets in every sample. + * + * This should only be called on an animation function that is inactive and + * that returns true from HasChanged(). + */ + void ClearHasChanged() { + MOZ_ASSERT(HasChanged(), + "clearing mHasChanged flag, when it's already false"); + MOZ_ASSERT(!IsActiveOrFrozen(), + "clearing mHasChanged flag for active animation"); + mHasChanged = false; + } + + /** + * Updates the cached record of our animation target, and returns a boolean + * that indicates whether the target has changed since the last call to this + * function. (This lets SMILCompositor check whether its animation + * functions have changed value or target since the last sample. If none of + * them have, then the compositor doesn't need to do anything.) + * + * @param aNewTarget A SMILTargetIdentifier representing the animation + * target of this function for this sample. + * @return true if |aNewTarget| is different from the old cached value; + * otherwise, false. + */ + bool UpdateCachedTarget(const SMILTargetIdentifier& aNewTarget); + + /** + * Returns true if this function was skipped in the previous sample (because + * there was a higher-priority non-additive animation). If a skipped animation + * function is later used, then the animation sandwich must be recomposited. + */ + bool WasSkippedInPrevSample() const { return mWasSkippedInPrevSample; } + + /** + * Mark this animation function as having been skipped. By marking the + * function as skipped, if it is used in a subsequent sample we'll know to + * recomposite the sandwich. + */ + void SetWasSkipped() { mWasSkippedInPrevSample = true; } + + /** + * Returns true if we need to recalculate the animation value on every sample. + * (e.g. because it depends on context like the font-size) + */ + bool ValueNeedsReparsingEverySample() const { + return mValueNeedsReparsingEverySample; + } + + // Comparator utility class, used for sorting SMILAnimationFunctions + class Comparator { + public: + bool Equals(const SMILAnimationFunction* aElem1, + const SMILAnimationFunction* aElem2) const { + return (aElem1->CompareTo(aElem2) == 0); + } + bool LessThan(const SMILAnimationFunction* aElem1, + const SMILAnimationFunction* aElem2) const { + return (aElem1->CompareTo(aElem2) < 0); + } + }; + + protected: + // alias declarations + using SMILValueArray = FallibleTArray<SMILValue>; + + // Types + enum SMILCalcMode : uint8_t { + CALC_LINEAR, + CALC_DISCRETE, + CALC_PACED, + CALC_SPLINE + }; + + // Used for sorting SMILAnimationFunctions + SMILTime GetBeginTime() const { return mBeginTime; } + + // Property getters + bool GetAccumulate() const; + bool GetAdditive() const; + virtual SMILCalcMode GetCalcMode() const; + + // Property setters + nsresult SetAccumulate(const nsAString& aAccumulate, nsAttrValue& aResult); + nsresult SetAdditive(const nsAString& aAdditive, nsAttrValue& aResult); + nsresult SetCalcMode(const nsAString& aCalcMode, nsAttrValue& aResult); + nsresult SetKeyTimes(const nsAString& aKeyTimes, nsAttrValue& aResult); + nsresult SetKeySplines(const nsAString& aKeySplines, nsAttrValue& aResult); + + // Property un-setters + void UnsetAccumulate(); + void UnsetAdditive(); + void UnsetCalcMode(); + void UnsetKeyTimes(); + void UnsetKeySplines(); + + // Helpers + virtual nsresult InterpolateResult(const SMILValueArray& aValues, + SMILValue& aResult, SMILValue& aBaseValue); + nsresult AccumulateResult(const SMILValueArray& aValues, SMILValue& aResult); + + nsresult ComputePacedPosition(const SMILValueArray& aValues, + double aSimpleProgress, + double& aIntervalProgress, + const SMILValue*& aFrom, const SMILValue*& aTo); + double ComputePacedTotalDistance(const SMILValueArray& aValues) const; + + /** + * Adjust the simple progress, that is, the point within the simple duration, + * by applying any keyTimes. + */ + double ScaleSimpleProgress(double aProgress, SMILCalcMode aCalcMode); + /** + * Adjust the progress within an interval, that is, between two animation + * values, by applying any keySplines. + */ + double ScaleIntervalProgress(double aProgress, uint32_t aIntervalIndex); + + // Convenience attribute getters -- use these instead of querying + // mAnimationElement as these may need to be overridden by subclasses + virtual bool HasAttr(nsAtom* aAttName) const; + virtual const nsAttrValue* GetAttr(nsAtom* aAttName) const; + virtual bool GetAttr(nsAtom* aAttName, nsAString& aResult) const; + + bool ParseAttr(nsAtom* aAttName, const SMILAttr& aSMILAttr, + SMILValue& aResult, bool& aPreventCachingOfSandwich) const; + + virtual nsresult GetValues(const SMILAttr& aSMILAttr, + SMILValueArray& aResult); + + virtual void CheckValueListDependentAttrs(uint32_t aNumValues); + void CheckKeyTimes(uint32_t aNumValues); + void CheckKeySplines(uint32_t aNumValues); + + virtual bool IsToAnimation() const { + return !HasAttr(nsGkAtoms::values) && HasAttr(nsGkAtoms::to) && + !HasAttr(nsGkAtoms::from); + } + + // Returns true if we know our composited value won't change over the + // simple duration of this animation (for a fixed base value). + virtual bool IsValueFixedForSimpleDuration() const; + + inline bool IsAdditive() const { + /* + * Animation is additive if: + * + * (1) additive = "sum" (GetAdditive() == true), or + * (2) it is 'by animation' (by is set, from and values are not) + * + * Although animation is not additive if it is 'to animation' + */ + bool isByAnimation = (!HasAttr(nsGkAtoms::values) && + HasAttr(nsGkAtoms::by) && !HasAttr(nsGkAtoms::from)); + return !IsToAnimation() && (GetAdditive() || isByAnimation); + } + + // Setters for error flags + // These correspond to bit-indices in mErrorFlags, for tracking parse errors + // in these attributes, when those parse errors should block us from doing + // animation. + enum AnimationAttributeIdx { + BF_ACCUMULATE = 0, + BF_ADDITIVE = 1, + BF_CALC_MODE = 2, + BF_KEY_TIMES = 3, + BF_KEY_SPLINES = 4, + BF_KEY_POINTS = 5 // <animateMotion> only + }; + + inline void SetAccumulateErrorFlag(bool aNewValue) { + SetErrorFlag(BF_ACCUMULATE, aNewValue); + } + inline void SetAdditiveErrorFlag(bool aNewValue) { + SetErrorFlag(BF_ADDITIVE, aNewValue); + } + inline void SetCalcModeErrorFlag(bool aNewValue) { + SetErrorFlag(BF_CALC_MODE, aNewValue); + } + inline void SetKeyTimesErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_TIMES, aNewValue); + } + inline void SetKeySplinesErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_SPLINES, aNewValue); + } + inline void SetKeyPointsErrorFlag(bool aNewValue) { + SetErrorFlag(BF_KEY_POINTS, aNewValue); + } + inline void SetErrorFlag(AnimationAttributeIdx aField, bool aValue) { + if (aValue) { + mErrorFlags |= (0x01 << aField); + } else { + mErrorFlags &= ~(0x01 << aField); + } + } + + // Members + // ------- + + static nsAttrValue::EnumTable sAdditiveTable[]; + static nsAttrValue::EnumTable sCalcModeTable[]; + static nsAttrValue::EnumTable sAccumulateTable[]; + + FallibleTArray<double> mKeyTimes; + FallibleTArray<SMILKeySpline> mKeySplines; + + // These are the parameters provided by the previous sample. Currently we + // perform lazy calculation. That is, we only calculate the result if and when + // instructed by the compositor. This allows us to apply the result directly + // to the animation value and allows the compositor to filter out functions + // that it determines will not contribute to the final result. + SMILTime mSampleTime; // sample time within simple dur + SMILTimeValue mSimpleDuration; + uint32_t mRepeatIteration; + + SMILTime mBeginTime; // document time + + // The owning animation element. This is used for sorting based on document + // position and for fetching attribute values stored in the element. + // Raw pointer is OK here, because this SMILAnimationFunction can't outlive + // its owning animation element. + mozilla::dom::SVGAnimationElement* mAnimationElement; + + // Which attributes have been set but have had errors. This is not used for + // all attributes but only those which have specified error behaviour + // associated with them. + uint16_t mErrorFlags; + + // Allows us to check whether an animation function has changed target from + // sample to sample (because if neither target nor animated value have + // changed, we don't have to do anything). + SMILWeakTargetIdentifier mLastTarget; + + // Boolean flags + bool mIsActive : 1; + bool mIsFrozen : 1; + bool mLastValue : 1; + bool mHasChanged : 1; + bool mValueNeedsReparsingEverySample : 1; + bool mPrevSampleWasSingleValueAnimation : 1; + bool mWasSkippedInPrevSample : 1; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILANIMATIONFUNCTION_H_ diff --git a/dom/smil/SMILAttr.h b/dom/smil/SMILAttr.h new file mode 100644 index 0000000000..a67921a9ad --- /dev/null +++ b/dom/smil/SMILAttr.h @@ -0,0 +1,100 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILATTR_H_ +#define DOM_SMIL_SMILATTR_H_ + +#include "nscore.h" +#include "nsStringFwd.h" + +class nsIContent; + +namespace mozilla { + +class SMILValue; + +namespace dom { +class SVGAnimationElement; +} // namespace dom + +//////////////////////////////////////////////////////////////////////// +// SMILAttr: A variable targeted by SMIL for animation and can therefore have +// an underlying (base) value and an animated value For example, an attribute of +// a particular SVG element. +// +// These objects only exist during the compositing phase of SMIL animation +// calculations. They have a single owner who is responsible for deleting the +// object. + +class SMILAttr { + public: + /** + * Creates a new SMILValue for this attribute from a string. The string is + * parsed in the context of this attribute so that context-dependent values + * such as em-based units can be resolved into a canonical form suitable for + * animation (including interpolation etc.). + * + * @param aStr A string defining the new value to be created. + * @param aSrcElement The source animation element. This may be needed to + * provided additional context data such as for + * animateTransform where the 'type' attribute is needed to + * parse the value. + * @param[out] aValue Outparam for storing the parsed value. + * @param[out] aPreventCachingOfSandwich + * Outparam to indicate whether the attribute contains + * dependencies on its context that should prevent the + * result of the animation sandwich from being cached and + * reused in future samples. + * @return NS_OK on success or an error code if creation failed. + */ + virtual nsresult ValueFromString( + const nsAString& aStr, + const mozilla::dom::SVGAnimationElement* aSrcElement, SMILValue& aValue, + bool& aPreventCachingOfSandwich) const = 0; + + /** + * Gets the underlying value of this attribute. + * + * @return a SMILValue object. returned_object.IsNull() will be true if an + * error occurred. + */ + virtual SMILValue GetBaseValue() const = 0; + + /** + * Clears the animated value of this attribute. + * + * NOTE: The animation target is not guaranteed to be in a document when this + * method is called. (See bug 523188) + */ + virtual void ClearAnimValue() = 0; + + /** + * Sets the presentation value of this attribute. + * + * @param aValue The value to set. + * @return NS_OK on success or an error code if setting failed. + */ + virtual nsresult SetAnimValue(const SMILValue& aValue) = 0; + + /** + * Returns the targeted content node, for any SMILAttr implementations + * that want to expose that to the animation logic. Otherwise, returns + * null. + * + * @return the targeted content node, if this SMILAttr implementation + * wishes to make it avaiable. Otherwise, nullptr. + */ + virtual const nsIContent* GetTargetNode() const { return nullptr; } + + /** + * Virtual destructor, to make sure subclasses can clean themselves up. + */ + virtual ~SMILAttr() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILATTR_H_ diff --git a/dom/smil/SMILBoolType.cpp b/dom/smil/SMILBoolType.cpp new file mode 100644 index 0000000000..218b7af16f --- /dev/null +++ b/dom/smil/SMILBoolType.cpp @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILBoolType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include <math.h> + +namespace mozilla { + +void SMILBoolType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mBool = false; + aValue.mType = this; +} + +void SMILBoolType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mBool = false; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILBoolType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mBool = aSrc.mU.mBool; + return NS_OK; +} + +bool SMILBoolType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mBool == aRight.mU.mBool; +} + +nsresult SMILBoolType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // bool values can't be added to each other +} + +nsresult SMILBoolType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between bool + // values +} + +nsresult SMILBoolType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // bool values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILBoolType.h b/dom/smil/SMILBoolType.h new file mode 100644 index 0000000000..a8c12a6860 --- /dev/null +++ b/dom/smil/SMILBoolType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILBOOLTYPE_H_ +#define DOM_SMIL_SMILBOOLTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILBoolType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILBoolType* Singleton() { + static SMILBoolType sSingleton; + return &sSingleton; + } + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILBoolType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILBOOLTYPE_H_ diff --git a/dom/smil/SMILCSSProperty.cpp b/dom/smil/SMILCSSProperty.cpp new file mode 100644 index 0000000000..5e4adc45f9 --- /dev/null +++ b/dom/smil/SMILCSSProperty.cpp @@ -0,0 +1,197 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a SMIL-animatable CSS property on an element */ + +#include "SMILCSSProperty.h" + +#include <utility> + +#include "mozilla/SMILCSSValueType.h" +#include "mozilla/SMILValue.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/dom/Element.h" +#include "nsCSSProps.h" +#include "nsDOMCSSAttrDeclaration.h" + +namespace mozilla { + +// Class Methods +SMILCSSProperty::SMILCSSProperty(nsCSSPropertyID aPropID, + dom::Element* aElement, + const ComputedStyle* aBaseComputedStyle) + : mPropID(aPropID), + mElement(aElement), + mBaseComputedStyle(aBaseComputedStyle) { + MOZ_ASSERT(IsPropertyAnimatable(mPropID), + "Creating a SMILCSSProperty for a property " + "that's not supported for animation"); +} + +SMILValue SMILCSSProperty::GetBaseValue() const { + // To benefit from Return Value Optimization and avoid copy constructor calls + // due to our use of return-by-value, we must return the exact same object + // from ALL return points. This function must only return THIS variable: + SMILValue baseValue; + + // SPECIAL CASE: (a) Shorthands + // (b) 'display' + // (c) No base ComputedStyle + if (nsCSSProps::IsShorthand(mPropID) || mPropID == eCSSProperty_display || + !mBaseComputedStyle) { + // We can't look up the base (computed-style) value of shorthand + // properties because they aren't guaranteed to have a consistent computed + // value. + // + // Also, although we can look up the base value of the display property, + // doing so involves clearing and resetting the property which can cause + // frames to be recreated which we'd like to avoid. + // + // Furthermore, if we don't (yet) have a base ComputedStyle we obviously + // can't resolve a base value. + // + // In any case, just return a dummy value (initialized with the right + // type, so as not to indicate failure). + SMILValue tmpVal(&SMILCSSValueType::sSingleton); + std::swap(baseValue, tmpVal); + return baseValue; + } + + AnimationValue computedValue; + computedValue.mServo = + Servo_ComputedValues_ExtractAnimationValue(mBaseComputedStyle, mPropID) + .Consume(); + if (!computedValue.mServo) { + return baseValue; + } + + baseValue = SMILCSSValueType::ValueFromAnimationValue(mPropID, mElement, + computedValue); + return baseValue; +} + +nsresult SMILCSSProperty::ValueFromString( + const nsAString& aStr, const dom::SVGAnimationElement* aSrcElement, + SMILValue& aValue, bool& aPreventCachingOfSandwich) const { + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + + SMILCSSValueType::ValueFromString(mPropID, mElement, aStr, aValue, + &aPreventCachingOfSandwich); + + if (aValue.IsNull()) { + return NS_ERROR_FAILURE; + } + + // XXX Due to bug 536660 (or at least that seems to be the most likely + // culprit), when we have animation setting display:none on a <use> element, + // if we DON'T set the property every sample, chaos ensues. + if (!aPreventCachingOfSandwich && mPropID == eCSSProperty_display) { + aPreventCachingOfSandwich = true; + } + return NS_OK; +} + +nsresult SMILCSSProperty::SetAnimValue(const SMILValue& aValue) { + NS_ENSURE_TRUE(IsPropertyAnimatable(mPropID), NS_ERROR_FAILURE); + return mElement->SMILOverrideStyle()->SetSMILValue(mPropID, aValue); +} + +void SMILCSSProperty::ClearAnimValue() { + // Put empty string in override style for our property + mElement->SMILOverrideStyle()->SetPropertyValue(mPropID, ""_ns, nullptr, + IgnoreErrors()); +} + +// Based on http://www.w3.org/TR/SVG/propidx.html +// static +bool SMILCSSProperty::IsPropertyAnimatable(nsCSSPropertyID aPropID) { + // NOTE: Right now, Gecko doesn't recognize the following properties from + // the SVG Property Index: + // alignment-baseline + // baseline-shift + // color-profile + // glyph-orientation-horizontal + // glyph-orientation-vertical + // kerning + // writing-mode + + switch (aPropID) { + case eCSSProperty_clip: + case eCSSProperty_clip_rule: + case eCSSProperty_clip_path: + case eCSSProperty_color: + case eCSSProperty_color_interpolation: + case eCSSProperty_color_interpolation_filters: + case eCSSProperty_cursor: + case eCSSProperty_display: + case eCSSProperty_dominant_baseline: + case eCSSProperty_fill: + case eCSSProperty_fill_opacity: + case eCSSProperty_fill_rule: + case eCSSProperty_filter: + case eCSSProperty_flood_color: + case eCSSProperty_flood_opacity: + case eCSSProperty_font: + case eCSSProperty_font_family: + case eCSSProperty_font_size: + case eCSSProperty_font_size_adjust: + case eCSSProperty_font_stretch: + case eCSSProperty_font_style: + case eCSSProperty_font_variant: + case eCSSProperty_font_weight: + case eCSSProperty_height: + case eCSSProperty_image_rendering: + case eCSSProperty_letter_spacing: + case eCSSProperty_lighting_color: + case eCSSProperty_marker: + case eCSSProperty_marker_end: + case eCSSProperty_marker_mid: + case eCSSProperty_marker_start: + case eCSSProperty_mask: + case eCSSProperty_mask_type: + case eCSSProperty_opacity: + case eCSSProperty_overflow: + case eCSSProperty_pointer_events: + case eCSSProperty_shape_rendering: + case eCSSProperty_stop_color: + case eCSSProperty_stop_opacity: + case eCSSProperty_stroke: + case eCSSProperty_stroke_dasharray: + case eCSSProperty_stroke_dashoffset: + case eCSSProperty_stroke_linecap: + case eCSSProperty_stroke_linejoin: + case eCSSProperty_stroke_miterlimit: + case eCSSProperty_stroke_opacity: + case eCSSProperty_stroke_width: + case eCSSProperty_text_anchor: + case eCSSProperty_text_decoration: + case eCSSProperty_text_decoration_line: + case eCSSProperty_text_rendering: + case eCSSProperty_vector_effect: + case eCSSProperty_width: + case eCSSProperty_visibility: + case eCSSProperty_word_spacing: + return true; + + // EXPLICITLY NON-ANIMATABLE PROPERTIES: + // (Some of these aren't supported at all in Gecko -- I've commented those + // ones out. If/when we add support for them, uncomment their line here) + // ---------------------------------------------------------------------- + // case eCSSProperty_enable_background: + // case eCSSProperty_glyph_orientation_horizontal: + // case eCSSProperty_glyph_orientation_vertical: + // case eCSSProperty_writing_mode: + case eCSSProperty_direction: + case eCSSProperty_unicode_bidi: + return false; + + default: + return false; + } +} + +} // namespace mozilla diff --git a/dom/smil/SMILCSSProperty.h b/dom/smil/SMILCSSProperty.h new file mode 100644 index 0000000000..d83721e56b --- /dev/null +++ b/dom/smil/SMILCSSProperty.h @@ -0,0 +1,80 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a SMIL-animatable CSS property on an element */ + +#ifndef DOM_SMIL_SMILCSSPROPERTY_H_ +#define DOM_SMIL_SMILCSSPROPERTY_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILAttr.h" +#include "nsAtom.h" +#include "nsCSSPropertyID.h" +#include "nsCSSValue.h" + +namespace mozilla { +class ComputedStyle; +namespace dom { +class Element; +} // namespace dom + +/** + * SMILCSSProperty: Implements the SMILAttr interface for SMIL animations + * that target CSS properties. Represents a particular animation-targeted CSS + * property on a particular element. + */ +class SMILCSSProperty : public SMILAttr { + public: + /** + * Constructs a new SMILCSSProperty. + * @param aPropID The CSS property we're interested in animating. + * @param aElement The element whose CSS property is being animated. + * @param aBaseComputedStyle The ComputedStyle to use when getting the base + * value. If this is nullptr and GetBaseValue is + * called, an empty SMILValue initialized with + * the SMILCSSValueType will be returned. + */ + SMILCSSProperty(nsCSSPropertyID aPropID, dom::Element* aElement, + const ComputedStyle* aBaseComputedStyle); + + // SMILAttr methods + nsresult ValueFromString(const nsAString& aStr, + const dom::SVGAnimationElement* aSrcElement, + SMILValue& aValue, + bool& aPreventCachingOfSandwich) const override; + SMILValue GetBaseValue() const override; + nsresult SetAnimValue(const SMILValue& aValue) override; + void ClearAnimValue() override; + + /** + * Utility method - returns true if the given property is supported for + * SMIL animation. + * + * @param aProperty The property to check for animation support. + * @return true if the given property is supported for SMIL animation, or + * false otherwise + */ + static bool IsPropertyAnimatable(nsCSSPropertyID aPropID); + + protected: + nsCSSPropertyID mPropID; + // Using non-refcounted pointer for mElement -- we know mElement will stay + // alive for my lifetime because a SMILAttr (like me) only lives as long + // as the Compositing step, and DOM elements don't get a chance to die during + // that time. + dom::Element* mElement; + + // The style to use when fetching base styles. + // + // As with mElement, since a SMILAttr only lives as long as the + // compositing step and since ComposeAttribute holds an owning reference to + // the base ComputedStyle, we can use a non-owning reference here. + const ComputedStyle* mBaseComputedStyle; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILCSSPROPERTY_H_ diff --git a/dom/smil/SMILCSSValueType.cpp b/dom/smil/SMILCSSValueType.cpp new file mode 100644 index 0000000000..d676f64d27 --- /dev/null +++ b/dom/smil/SMILCSSValueType.cpp @@ -0,0 +1,573 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a value for a SMIL-animated CSS property */ + +#include "SMILCSSValueType.h" + +#include "nsComputedDOMStyle.h" +#include "nsColor.h" +#include "nsCSSProps.h" +#include "nsCSSValue.h" +#include "nsDebug.h" +#include "nsPresContextInlines.h" +#include "nsPresContext.h" +#include "nsString.h" +#include "nsStyleUtil.h" +#include "mozilla/DeclarationBlock.h" +#include "mozilla/PresShell.h" +#include "mozilla/PresShellInlines.h" +#include "mozilla/ServoBindings.h" +#include "mozilla/StyleAnimationValue.h" +#include "mozilla/ServoCSSParser.h" +#include "mozilla/ServoStyleSet.h" +#include "mozilla/SMILParserUtils.h" +#include "mozilla/SMILValue.h" +#include "mozilla/dom/BaseKeyframeTypesBinding.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Element.h" + +using namespace mozilla::dom; + +namespace mozilla { + +using ServoAnimationValues = + CopyableAutoTArray<RefPtr<RawServoAnimationValue>, 1>; + +/*static*/ +SMILCSSValueType SMILCSSValueType::sSingleton; + +struct ValueWrapper { + ValueWrapper(nsCSSPropertyID aPropID, const AnimationValue& aValue) + : mPropID(aPropID) { + MOZ_ASSERT(!aValue.IsNull()); + mServoValues.AppendElement(aValue.mServo); + } + ValueWrapper(nsCSSPropertyID aPropID, + const RefPtr<RawServoAnimationValue>& aValue) + : mPropID(aPropID), mServoValues{(aValue)} {} + ValueWrapper(nsCSSPropertyID aPropID, ServoAnimationValues&& aValues) + : mPropID(aPropID), mServoValues{std::move(aValues)} {} + + bool operator==(const ValueWrapper& aOther) const { + if (mPropID != aOther.mPropID) { + return false; + } + + MOZ_ASSERT(!mServoValues.IsEmpty()); + size_t len = mServoValues.Length(); + if (len != aOther.mServoValues.Length()) { + return false; + } + for (size_t i = 0; i < len; i++) { + if (!Servo_AnimationValue_DeepEqual(mServoValues[i], + aOther.mServoValues[i])) { + return false; + } + } + return true; + } + + bool operator!=(const ValueWrapper& aOther) const { + return !(*this == aOther); + } + + nsCSSPropertyID mPropID; + ServoAnimationValues mServoValues; +}; + +// Helper Methods +// -------------- + +// If one argument is null, this method updates it to point to "zero" +// for the other argument's Unit (if applicable; otherwise, we return false). +// +// If neither argument is null, this method simply returns true. +// +// If both arguments are null, this method returns false. +// +// |aZeroValueStorage| should be a reference to a +// RefPtr<RawServoAnimationValue>. This is used where we may need to allocate a +// new ServoAnimationValue to represent the appropriate zero value. +// +// Returns true on success, or otherwise. +static bool FinalizeServoAnimationValues( + const RefPtr<RawServoAnimationValue>*& aValue1, + const RefPtr<RawServoAnimationValue>*& aValue2, + RefPtr<RawServoAnimationValue>& aZeroValueStorage) { + if (!aValue1 && !aValue2) { + return false; + } + + // Are we missing either val? (If so, it's an implied 0 in other val's units) + + if (!aValue1) { + aZeroValueStorage = Servo_AnimationValues_GetZeroValue(*aValue2).Consume(); + aValue1 = &aZeroValueStorage; + } else if (!aValue2) { + aZeroValueStorage = Servo_AnimationValues_GetZeroValue(*aValue1).Consume(); + aValue2 = &aZeroValueStorage; + } + return *aValue1 && *aValue2; +} + +static ValueWrapper* ExtractValueWrapper(SMILValue& aValue) { + return static_cast<ValueWrapper*>(aValue.mU.mPtr); +} + +static const ValueWrapper* ExtractValueWrapper(const SMILValue& aValue) { + return static_cast<const ValueWrapper*>(aValue.mU.mPtr); +} + +// Class methods +// ------------- +void SMILCSSValueType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected SMIL value type"); + + aValue.mU.mPtr = nullptr; + aValue.mType = this; +} + +void SMILCSSValueType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value type"); + delete static_cast<ValueWrapper*>(aValue.mU.mPtr); + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILCSSValueType::Assign(SMILValue& aDest, + const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value type"); + const ValueWrapper* srcWrapper = ExtractValueWrapper(aSrc); + ValueWrapper* destWrapper = ExtractValueWrapper(aDest); + + if (srcWrapper) { + if (!destWrapper) { + // barely-initialized dest -- need to alloc & copy + aDest.mU.mPtr = new ValueWrapper(*srcWrapper); + } else { + // both already fully-initialized -- just copy straight across + *destWrapper = *srcWrapper; + } + } else if (destWrapper) { + // fully-initialized dest, barely-initialized src -- clear dest + delete destWrapper; + aDest.mU.mPtr = destWrapper = nullptr; + } // else, both are barely-initialized -- nothing to do. + + return NS_OK; +} + +bool SMILCSSValueType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected SMIL value"); + const ValueWrapper* leftWrapper = ExtractValueWrapper(aLeft); + const ValueWrapper* rightWrapper = ExtractValueWrapper(aRight); + + if (leftWrapper) { + if (rightWrapper) { + // Both non-null + NS_WARNING_ASSERTION(leftWrapper != rightWrapper, + "Two SMILValues with matching ValueWrapper ptr"); + return *leftWrapper == *rightWrapper; + } + // Left non-null, right null + return false; + } + if (rightWrapper) { + // Left null, right non-null + return false; + } + // Both null + return true; +} + +static bool AddOrAccumulateForServo(SMILValue& aDest, + const ValueWrapper* aValueToAddWrapper, + ValueWrapper* aDestWrapper, + CompositeOperation aCompositeOp, + uint64_t aCount) { + nsCSSPropertyID property = + aValueToAddWrapper ? aValueToAddWrapper->mPropID : aDestWrapper->mPropID; + size_t len = aValueToAddWrapper ? aValueToAddWrapper->mServoValues.Length() + : aDestWrapper->mServoValues.Length(); + + MOZ_ASSERT(!aValueToAddWrapper || !aDestWrapper || + aValueToAddWrapper->mServoValues.Length() == + aDestWrapper->mServoValues.Length(), + "Both of values' length in the wrappers should be the same if " + "both of them exist"); + + for (size_t i = 0; i < len; i++) { + const RefPtr<RawServoAnimationValue>* valueToAdd = + aValueToAddWrapper ? &aValueToAddWrapper->mServoValues[i] : nullptr; + const RefPtr<RawServoAnimationValue>* destValue = + aDestWrapper ? &aDestWrapper->mServoValues[i] : nullptr; + RefPtr<RawServoAnimationValue> zeroValueStorage; + if (!FinalizeServoAnimationValues(valueToAdd, destValue, + zeroValueStorage)) { + return false; + } + + // FinalizeServoAnimationValues may have updated destValue so we should make + // sure the aDest and aDestWrapper outparams are up-to-date. + if (aDestWrapper) { + aDestWrapper->mServoValues[i] = *destValue; + } else { + // aDest may be a barely-initialized "zero" destination. + aDest.mU.mPtr = aDestWrapper = new ValueWrapper(property, *destValue); + aDestWrapper->mServoValues.SetLength(len); + } + + RefPtr<RawServoAnimationValue> result; + if (aCompositeOp == CompositeOperation::Add) { + result = Servo_AnimationValues_Add(*destValue, *valueToAdd).Consume(); + } else { + result = Servo_AnimationValues_Accumulate(*destValue, *valueToAdd, aCount) + .Consume(); + } + + if (!result) { + return false; + } + aDestWrapper->mServoValues[i] = result; + } + + return true; +} + +static bool AddOrAccumulate(SMILValue& aDest, const SMILValue& aValueToAdd, + CompositeOperation aCompositeOp, uint64_t aCount) { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, + "Trying to add mismatching types"); + MOZ_ASSERT(aValueToAdd.mType == &SMILCSSValueType::sSingleton, + "Unexpected SMIL value type"); + MOZ_ASSERT(aCompositeOp == CompositeOperation::Add || + aCompositeOp == CompositeOperation::Accumulate, + "Composite operation should be add or accumulate"); + MOZ_ASSERT(aCompositeOp != CompositeOperation::Add || aCount == 1, + "Count should be 1 if composite operation is add"); + + ValueWrapper* destWrapper = ExtractValueWrapper(aDest); + const ValueWrapper* valueToAddWrapper = ExtractValueWrapper(aValueToAdd); + + // If both of the values are empty just fail. This can happen in rare cases + // such as when the underlying animation produced an empty value. + // + // Technically, it doesn't matter what we return here since in either case it + // will produce the same result: an empty value. + if (!destWrapper && !valueToAddWrapper) { + return false; + } + + nsCSSPropertyID property = + valueToAddWrapper ? valueToAddWrapper->mPropID : destWrapper->mPropID; + // Special case: font-size-adjust and stroke-dasharray are explicitly + // non-additive (even though StyleAnimationValue *could* support adding them) + if (property == eCSSProperty_font_size_adjust || + property == eCSSProperty_stroke_dasharray) { + return false; + } + // Skip font shorthand since it includes font-size-adjust. + if (property == eCSSProperty_font) { + return false; + } + + return AddOrAccumulateForServo(aDest, valueToAddWrapper, destWrapper, + aCompositeOp, aCount); +} + +nsresult SMILCSSValueType::SandwichAdd(SMILValue& aDest, + const SMILValue& aValueToAdd) const { + return AddOrAccumulate(aDest, aValueToAdd, CompositeOperation::Add, 1) + ? NS_OK + : NS_ERROR_FAILURE; +} + +nsresult SMILCSSValueType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + return AddOrAccumulate(aDest, aValueToAdd, CompositeOperation::Accumulate, + aCount) + ? NS_OK + : NS_ERROR_FAILURE; +} + +static nsresult ComputeDistanceForServo(const ValueWrapper* aFromWrapper, + const ValueWrapper& aToWrapper, + double& aDistance) { + size_t len = aToWrapper.mServoValues.Length(); + MOZ_ASSERT(!aFromWrapper || aFromWrapper->mServoValues.Length() == len, + "From and to values length should be the same if " + "The start value exists"); + + double squareDistance = 0; + + for (size_t i = 0; i < len; i++) { + const RefPtr<RawServoAnimationValue>* fromValue = + aFromWrapper ? &aFromWrapper->mServoValues[0] : nullptr; + const RefPtr<RawServoAnimationValue>* toValue = &aToWrapper.mServoValues[0]; + RefPtr<RawServoAnimationValue> zeroValueStorage; + if (!FinalizeServoAnimationValues(fromValue, toValue, zeroValueStorage)) { + return NS_ERROR_FAILURE; + } + + double distance = + Servo_AnimationValues_ComputeDistance(*fromValue, *toValue); + if (distance < 0.0) { + return NS_ERROR_FAILURE; + } + + if (len == 1) { + aDistance = distance; + return NS_OK; + } + squareDistance += distance * distance; + } + + aDistance = sqrt(squareDistance); + + return NS_OK; +} + +nsresult SMILCSSValueType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + + const ValueWrapper* fromWrapper = ExtractValueWrapper(aFrom); + const ValueWrapper* toWrapper = ExtractValueWrapper(aTo); + MOZ_ASSERT(toWrapper, "expecting non-null endpoint"); + return ComputeDistanceForServo(fromWrapper, *toWrapper, aDistance); +} + +static nsresult InterpolateForServo(const ValueWrapper* aStartWrapper, + const ValueWrapper& aEndWrapper, + double aUnitDistance, SMILValue& aResult) { + // For discretely-animated properties Servo_AnimationValues_Interpolate will + // perform the discrete animation (i.e. 50% flip) and return a success result. + // However, SMIL has its own special discrete animation behavior that it uses + // when keyTimes are specified, but we won't run that unless that this method + // returns a failure to indicate that the property cannot be smoothly + // interpolated, i.e. that we need to use a discrete calcMode. + // + // For shorthands, Servo_Property_IsDiscreteAnimatable will always return + // false. That's fine since most shorthands (like 'font' and + // 'text-decoration') include non-discrete components. If authors want to + // treat all components as discrete then they should use calcMode="discrete". + if (Servo_Property_IsDiscreteAnimatable(aEndWrapper.mPropID)) { + return NS_ERROR_FAILURE; + } + + ServoAnimationValues results; + size_t len = aEndWrapper.mServoValues.Length(); + results.SetCapacity(len); + MOZ_ASSERT(!aStartWrapper || aStartWrapper->mServoValues.Length() == len, + "Start and end values length should be the same if " + "the start value exists"); + for (size_t i = 0; i < len; i++) { + const RefPtr<RawServoAnimationValue>* startValue = + aStartWrapper ? &aStartWrapper->mServoValues[i] : nullptr; + const RefPtr<RawServoAnimationValue>* endValue = + &aEndWrapper.mServoValues[i]; + RefPtr<RawServoAnimationValue> zeroValueStorage; + if (!FinalizeServoAnimationValues(startValue, endValue, zeroValueStorage)) { + return NS_ERROR_FAILURE; + } + + RefPtr<RawServoAnimationValue> result = + Servo_AnimationValues_Interpolate(*startValue, *endValue, aUnitDistance) + .Consume(); + if (!result) { + return NS_ERROR_FAILURE; + } + results.AppendElement(result); + } + aResult.mU.mPtr = new ValueWrapper(aEndWrapper.mPropID, std::move(results)); + + return NS_OK; +} + +nsresult SMILCSSValueType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + MOZ_ASSERT(aUnitDistance >= 0.0 && aUnitDistance <= 1.0, + "unit distance value out of bounds"); + MOZ_ASSERT(!aResult.mU.mPtr, "expecting barely-initialized outparam"); + + const ValueWrapper* startWrapper = ExtractValueWrapper(aStartVal); + const ValueWrapper* endWrapper = ExtractValueWrapper(aEndVal); + MOZ_ASSERT(endWrapper, "expecting non-null endpoint"); + return InterpolateForServo(startWrapper, *endWrapper, aUnitDistance, aResult); +} + +static ServoAnimationValues ValueFromStringHelper( + nsCSSPropertyID aPropID, Element* aTargetElement, + nsPresContext* aPresContext, const ComputedStyle* aComputedStyle, + const nsAString& aString) { + ServoAnimationValues result; + + Document* doc = aTargetElement->GetComposedDoc(); + if (!doc) { + return result; + } + + // Parse property + ServoCSSParser::ParsingEnvironment env = + ServoCSSParser::GetParsingEnvironment(doc); + RefPtr<RawServoDeclarationBlock> servoDeclarationBlock = + ServoCSSParser::ParseProperty(aPropID, NS_ConvertUTF16toUTF8(aString), + env, + ParsingMode::AllowUnitlessLength | + ParsingMode::AllowAllNumericValues); + if (!servoDeclarationBlock) { + return result; + } + + // Compute value + aPresContext->StyleSet()->GetAnimationValues( + servoDeclarationBlock, aTargetElement, aComputedStyle, result); + + return result; +} + +// static +void SMILCSSValueType::ValueFromString(nsCSSPropertyID aPropID, + Element* aTargetElement, + const nsAString& aString, + SMILValue& aValue, + bool* aIsContextSensitive) { + MOZ_ASSERT(aValue.IsNull(), "Outparam should be null-typed"); + nsPresContext* presContext = + nsContentUtils::GetContextForContent(aTargetElement); + if (!presContext) { + NS_WARNING("Not parsing animation value; unable to get PresContext"); + return; + } + + Document* doc = aTargetElement->GetComposedDoc(); + if (doc && !nsStyleUtil::CSPAllowsInlineStyle(nullptr, doc, nullptr, 0, 0, + aString, nullptr)) { + return; + } + + RefPtr<const ComputedStyle> computedStyle = + nsComputedDOMStyle::GetComputedStyle(aTargetElement); + if (!computedStyle) { + return; + } + + ServoAnimationValues parsedValues = ValueFromStringHelper( + aPropID, aTargetElement, presContext, computedStyle, aString); + if (aIsContextSensitive) { + // FIXME: Bug 1358955 - detect context-sensitive values and set this value + // appropriately. + *aIsContextSensitive = false; + } + + if (!parsedValues.IsEmpty()) { + sSingleton.Init(aValue); + aValue.mU.mPtr = new ValueWrapper(aPropID, std::move(parsedValues)); + } +} + +// static +SMILValue SMILCSSValueType::ValueFromAnimationValue( + nsCSSPropertyID aPropID, Element* aTargetElement, + const AnimationValue& aValue) { + SMILValue result; + + Document* doc = aTargetElement->GetComposedDoc(); + // We'd like to avoid serializing |aValue| if possible, and since the + // string passed to CSPAllowsInlineStyle is only used for reporting violations + // and an intermediate CSS value is not likely to be particularly useful + // in that case, we just use a generic placeholder string instead. + static const nsLiteralString kPlaceholderText = u"[SVG animation of CSS]"_ns; + if (doc && !nsStyleUtil::CSPAllowsInlineStyle(nullptr, doc, nullptr, 0, 0, + kPlaceholderText, nullptr)) { + return result; + } + + sSingleton.Init(result); + result.mU.mPtr = new ValueWrapper(aPropID, aValue); + + return result; +} + +// static +bool SMILCSSValueType::SetPropertyValues(const SMILValue& aValue, + DeclarationBlock& aDecl) { + MOZ_ASSERT(aValue.mType == &SMILCSSValueType::sSingleton, + "Unexpected SMIL value type"); + const ValueWrapper* wrapper = ExtractValueWrapper(aValue); + if (!wrapper) { + return false; + } + + bool changed = false; + for (const auto& value : wrapper->mServoValues) { + changed |= Servo_DeclarationBlock_SetPropertyToAnimationValue(aDecl.Raw(), + value, {}); + } + + return changed; +} + +// static +nsCSSPropertyID SMILCSSValueType::PropertyFromValue(const SMILValue& aValue) { + if (aValue.mType != &SMILCSSValueType::sSingleton) { + return eCSSProperty_UNKNOWN; + } + + const ValueWrapper* wrapper = ExtractValueWrapper(aValue); + if (!wrapper) { + return eCSSProperty_UNKNOWN; + } + + return wrapper->mPropID; +} + +// static +void SMILCSSValueType::FinalizeValue(SMILValue& aValue, + const SMILValue& aValueToMatch) { + MOZ_ASSERT(aValue.mType == aValueToMatch.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aValue.mType == &SMILCSSValueType::sSingleton, + "Unexpected SMIL value type"); + + ValueWrapper* valueWrapper = ExtractValueWrapper(aValue); + // If |aValue| already has a value, there's nothing to do here. + if (valueWrapper) { + return; + } + + const ValueWrapper* valueToMatchWrapper = ExtractValueWrapper(aValueToMatch); + if (!valueToMatchWrapper) { + MOZ_ASSERT_UNREACHABLE("Value to match is empty"); + return; + } + + ServoAnimationValues zeroValues; + zeroValues.SetCapacity(valueToMatchWrapper->mServoValues.Length()); + + for (auto& valueToMatch : valueToMatchWrapper->mServoValues) { + RefPtr<RawServoAnimationValue> zeroValue = + Servo_AnimationValues_GetZeroValue(valueToMatch).Consume(); + if (!zeroValue) { + return; + } + zeroValues.AppendElement(std::move(zeroValue)); + } + aValue.mU.mPtr = + new ValueWrapper(valueToMatchWrapper->mPropID, std::move(zeroValues)); +} + +} // namespace mozilla diff --git a/dom/smil/SMILCSSValueType.h b/dom/smil/SMILCSSValueType.h new file mode 100644 index 0000000000..72c66bdc43 --- /dev/null +++ b/dom/smil/SMILCSSValueType.h @@ -0,0 +1,133 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* representation of a value for a SMIL-animated CSS property */ + +#ifndef DOM_SMIL_SMILCSSVALUETYPE_H_ +#define DOM_SMIL_SMILCSSVALUETYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" +#include "nsCSSPropertyID.h" +#include "nsStringFwd.h" + +namespace mozilla { +struct AnimationValue; +class DeclarationBlock; +namespace dom { +class Element; +} // namespace dom + +/* + * SMILCSSValueType: Represents a SMIL-animated CSS value. + */ +class SMILCSSValueType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILCSSValueType sSingleton; + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue&) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult SandwichAdd(SMILValue& aDest, + const SMILValue& aValueToAdd) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + public: + // Helper Methods + // -------------- + /** + * Sets up the given SMILValue to represent the given string value. The + * string is interpreted as a value for the given property on the given + * element. + * + * On failure, this method leaves aValue.mType == SMILNullType::sSingleton. + * Otherwise, this method leaves aValue.mType == this class's singleton. + * + * @param aPropID The property for which we're parsing a value. + * @param aTargetElement The target element to whom the property/value + * setting applies. + * @param aString The string to be parsed as a CSS value. + * @param [out] aValue The SMILValue to be populated. Should + * initially be null-typed. + * @param [out] aIsContextSensitive Set to true if |aString| may produce + * a different |aValue| depending on other + * CSS properties on |aTargetElement| + * or its ancestors (e.g. 'inherit). + * false otherwise. May be nullptr. + * Not set if the method fails. + * @pre aValue.IsNull() + * @post aValue.IsNull() || aValue.mType == SMILCSSValueType::sSingleton + */ + static void ValueFromString(nsCSSPropertyID aPropID, + dom::Element* aTargetElement, + const nsAString& aString, SMILValue& aValue, + bool* aIsContextSensitive); + + /** + * Creates a SMILValue to wrap the given animation value. + * + * @param aPropID The property that |aValue| corresponds to. + * @param aTargetElement The target element to which the animation value + * applies. + * @param aValue The animation value to use. + * @return A new SMILValue. On failure, returns a + * SMILValue with the null type (i.e. rv.IsNull() + * returns true). + */ + static SMILValue ValueFromAnimationValue(nsCSSPropertyID aPropID, + dom::Element* aTargetElement, + const AnimationValue& aValue); + + /** + * Sets the relevant property values in the declaration block. + * + * Returns whether the declaration changed. + */ + static bool SetPropertyValues(const SMILValue&, mozilla::DeclarationBlock&); + + /** + * Return the CSS property animated by the specified value. + * + * @param aValue The SMILValue to examine. + * @return The nsCSSPropertyID enum value of the property animated + * by |aValue|, or eCSSProperty_UNKNOWN if the type of + * |aValue| is not SMILCSSValueType. + */ + static nsCSSPropertyID PropertyFromValue(const SMILValue& aValue); + + /** + * If |aValue| is an empty value, converts it to a suitable zero value by + * matching the type of value stored in |aValueToMatch|. + * + * There is no indication if this method fails. If a suitable zero value could + * not be created, |aValue| is simply unmodified. + * + * @param aValue The SMILValue (of type SMILCSSValueType) to + * possibly update. + * @param aValueToMatch A SMILValue (of type SMILCSSValueType) for which + * a corresponding zero value will be created if |aValue| + * is empty. + */ + static void FinalizeValue(SMILValue& aValue, const SMILValue& aValueToMatch); + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILCSSValueType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILCSSVALUETYPE_H_ diff --git a/dom/smil/SMILCompositor.cpp b/dom/smil/SMILCompositor.cpp new file mode 100644 index 0000000000..6b55267cae --- /dev/null +++ b/dom/smil/SMILCompositor.cpp @@ -0,0 +1,239 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILCompositor.h" + +#include "mozilla/dom/SVGSVGElement.h" +#include "nsComputedDOMStyle.h" +#include "nsCSSProps.h" +#include "nsHashKeys.h" +#include "SMILCSSProperty.h" + +namespace mozilla { + +// PLDHashEntryHdr methods +bool SMILCompositor::KeyEquals(KeyTypePointer aKey) const { + return aKey && aKey->Equals(mKey); +} + +/*static*/ +PLDHashNumber SMILCompositor::HashKey(KeyTypePointer aKey) { + // Combine the 3 values into one numeric value, which will be hashed. + // NOTE: We right-shift one of the pointers by 2 to get some randomness in + // its 2 lowest-order bits. (Those shifted-off bits will always be 0 since + // our pointers will be word-aligned.) + return (NS_PTR_TO_UINT32(aKey->mElement.get()) >> 2) + + NS_PTR_TO_UINT32(aKey->mAttributeName.get()); +} + +// Cycle-collection support +void SMILCompositor::Traverse(nsCycleCollectionTraversalCallback* aCallback) { + if (!mKey.mElement) return; + + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCallback, "Compositor mKey.mElement"); + aCallback->NoteXPCOMChild(mKey.mElement); +} + +// Other methods +void SMILCompositor::AddAnimationFunction(SMILAnimationFunction* aFunc) { + if (aFunc) { + mAnimationFunctions.AppendElement(aFunc); + } +} + +void SMILCompositor::ComposeAttribute(bool& aMightHavePendingStyleUpdates) { + if (!mKey.mElement) return; + + // If we might need to resolve base styles, grab a suitable ComputedStyle + // for initializing our SMILAttr with. + RefPtr<const ComputedStyle> baseComputedStyle; + if (MightNeedBaseStyle()) { + baseComputedStyle = nsComputedDOMStyle::GetUnanimatedComputedStyleNoFlush( + mKey.mElement, PseudoStyleType::NotPseudo); + } + + // FIRST: Get the SMILAttr (to grab base value from, and to eventually + // give animated value to) + UniquePtr<SMILAttr> smilAttr = CreateSMILAttr(baseComputedStyle); + if (!smilAttr) { + // Target attribute not found (or, out of memory) + return; + } + if (mAnimationFunctions.IsEmpty()) { + // No active animation functions. (We can still have a SMILCompositor in + // that case if an animation function has *just* become inactive) + smilAttr->ClearAnimValue(); + // Removing the animation effect may require a style update. + aMightHavePendingStyleUpdates = true; + return; + } + + // SECOND: Sort the animationFunctions, to prepare for compositing. + SMILAnimationFunction::Comparator comparator; + mAnimationFunctions.Sort(comparator); + + // THIRD: Step backwards through animation functions to find out + // which ones we actually care about. + uint32_t firstFuncToCompose = GetFirstFuncToAffectSandwich(); + + // FOURTH: Get & cache base value + SMILValue sandwichResultValue; + if (!mAnimationFunctions[firstFuncToCompose]->WillReplace()) { + sandwichResultValue = smilAttr->GetBaseValue(); + } + UpdateCachedBaseValue(sandwichResultValue); + + if (!mForceCompositing) { + return; + } + + // FIFTH: Compose animation functions + aMightHavePendingStyleUpdates = true; + uint32_t length = mAnimationFunctions.Length(); + for (uint32_t i = firstFuncToCompose; i < length; ++i) { + mAnimationFunctions[i]->ComposeResult(*smilAttr, sandwichResultValue); + } + if (sandwichResultValue.IsNull()) { + smilAttr->ClearAnimValue(); + return; + } + + // SIXTH: Set the animated value to the final composited result. + nsresult rv = smilAttr->SetAnimValue(sandwichResultValue); + if (NS_FAILED(rv)) { + NS_WARNING("SMILAttr::SetAnimValue failed"); + } +} + +void SMILCompositor::ClearAnimationEffects() { + if (!mKey.mElement || !mKey.mAttributeName) return; + + UniquePtr<SMILAttr> smilAttr = CreateSMILAttr(nullptr); + if (!smilAttr) { + // Target attribute not found (or, out of memory) + return; + } + smilAttr->ClearAnimValue(); +} + +// Protected Helper Functions +// -------------------------- +UniquePtr<SMILAttr> SMILCompositor::CreateSMILAttr( + const ComputedStyle* aBaseComputedStyle) { + nsCSSPropertyID propID = GetCSSPropertyToAnimate(); + + if (propID != eCSSProperty_UNKNOWN) { + return MakeUnique<SMILCSSProperty>(propID, mKey.mElement.get(), + aBaseComputedStyle); + } + + return mKey.mElement->GetAnimatedAttr(mKey.mAttributeNamespaceID, + mKey.mAttributeName); +} + +nsCSSPropertyID SMILCompositor::GetCSSPropertyToAnimate() const { + if (mKey.mAttributeNamespaceID != kNameSpaceID_None) { + return eCSSProperty_UNKNOWN; + } + + nsCSSPropertyID propID = + nsCSSProps::LookupProperty(nsAtomCString(mKey.mAttributeName)); + + if (!SMILCSSProperty::IsPropertyAnimatable(propID)) { + return eCSSProperty_UNKNOWN; + } + + // If we are animating the 'width' or 'height' of an outer SVG + // element we should animate it as a CSS property, but for other elements + // in SVG namespace (e.g. <rect>) we should animate it as a length attribute. + if ((mKey.mAttributeName == nsGkAtoms::width || + mKey.mAttributeName == nsGkAtoms::height) && + mKey.mElement->GetNameSpaceID() == kNameSpaceID_SVG) { + // Not an <svg> element. + if (!mKey.mElement->IsSVGElement(nsGkAtoms::svg)) { + return eCSSProperty_UNKNOWN; + } + + // An inner <svg> element + if (static_cast<dom::SVGSVGElement const&>(*mKey.mElement).IsInner()) { + return eCSSProperty_UNKNOWN; + } + + // Indeed an outer <svg> element, fall through. + } + + return propID; +} + +bool SMILCompositor::MightNeedBaseStyle() const { + if (GetCSSPropertyToAnimate() == eCSSProperty_UNKNOWN) { + return false; + } + + // We should return true if at least one animation function might build on + // the base value. + for (const SMILAnimationFunction* func : mAnimationFunctions) { + if (!func->WillReplace()) { + return true; + } + } + + return false; +} + +uint32_t SMILCompositor::GetFirstFuncToAffectSandwich() { + // For performance reasons, we throttle most animations on elements in + // display:none subtrees. (We can't throttle animations that target the + // "display" property itself, though -- if we did, display:none elements + // could never be dynamically displayed via animations.) + // To determine whether we're in a display:none subtree, we will check the + // element's primary frame since element in display:none subtree doesn't have + // a primary frame. Before this process, we will construct frame when we + // append an element to subtree. So we will not need to worry about pending + // frame construction in this step. + bool canThrottle = mKey.mAttributeName != nsGkAtoms::display && + !mKey.mElement->GetPrimaryFrame(); + + uint32_t i; + for (i = mAnimationFunctions.Length(); i > 0; --i) { + SMILAnimationFunction* curAnimFunc = mAnimationFunctions[i - 1]; + // In the following, the lack of short-circuit behavior of |= means that we + // will ALWAYS run UpdateCachedTarget (even if mForceCompositing is true) + // but only call HasChanged and WasSkippedInPrevSample if necessary. This + // is important since we need UpdateCachedTarget to run in order to detect + // changes to the target in subsequent samples. + mForceCompositing |= curAnimFunc->UpdateCachedTarget(mKey) || + (curAnimFunc->HasChanged() && !canThrottle) || + curAnimFunc->WasSkippedInPrevSample(); + + if (curAnimFunc->WillReplace()) { + --i; + break; + } + } + + // Mark remaining animation functions as having been skipped so if we later + // use them we'll know to force compositing. + // Note that we only really need to do this if something has changed + // (otherwise we would have set the flag on a previous sample) and if + // something has changed mForceCompositing will be true. + if (mForceCompositing) { + for (uint32_t j = i; j > 0; --j) { + mAnimationFunctions[j - 1]->SetWasSkipped(); + } + } + return i; +} + +void SMILCompositor::UpdateCachedBaseValue(const SMILValue& aBaseValue) { + if (mCachedBaseValue != aBaseValue) { + // Base value has changed since last sample. + mCachedBaseValue = aBaseValue; + mForceCompositing = true; + } +} + +} // namespace mozilla diff --git a/dom/smil/SMILCompositor.h b/dom/smil/SMILCompositor.h new file mode 100644 index 0000000000..85ab14557a --- /dev/null +++ b/dom/smil/SMILCompositor.h @@ -0,0 +1,128 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILCOMPOSITOR_H_ +#define DOM_SMIL_SMILCOMPOSITOR_H_ + +#include <utility> + +#include "PLDHashTable.h" +#include "SMILTargetIdentifier.h" +#include "mozilla/SMILAnimationFunction.h" +#include "mozilla/SMILCompositorTable.h" +#include "mozilla/UniquePtr.h" +#include "nsCSSPropertyID.h" +#include "nsString.h" +#include "nsTHashtable.h" + +namespace mozilla { + +class ComputedStyle; + +//---------------------------------------------------------------------- +// SMILCompositor +// +// Performs the composition of the animation sandwich by combining the results +// of a series animation functions according to the rules of SMIL composition +// including prioritising animations. + +class SMILCompositor : public PLDHashEntryHdr { + public: + using KeyType = SMILTargetIdentifier; + using KeyTypeRef = const KeyType&; + using KeyTypePointer = const KeyType*; + + explicit SMILCompositor(KeyTypePointer aKey) + : mKey(*aKey), mForceCompositing(false) {} + SMILCompositor(SMILCompositor&& toMove) noexcept + : PLDHashEntryHdr(std::move(toMove)), + mKey(std::move(toMove.mKey)), + mAnimationFunctions(std::move(toMove.mAnimationFunctions)), + mForceCompositing(false) {} + + // PLDHashEntryHdr methods + KeyTypeRef GetKey() const { return mKey; } + bool KeyEquals(KeyTypePointer aKey) const; + static KeyTypePointer KeyToPointer(KeyTypeRef aKey) { return &aKey; } + static PLDHashNumber HashKey(KeyTypePointer aKey); + enum { ALLOW_MEMMOVE = false }; + + // Adds the given animation function to this Compositor's list of functions + void AddAnimationFunction(SMILAnimationFunction* aFunc); + + // Composes the attribute's current value with the list of animation + // functions, and assigns the resulting value to this compositor's target + // attribute. If a change is made that might produce style updates, + // aMightHavePendingStyleUpdates is set to true. Otherwise it is not modified. + void ComposeAttribute(bool& aMightHavePendingStyleUpdates); + + // Clears animation effects on my target attribute + void ClearAnimationEffects(); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + + // Toggles a bit that will force us to composite (bypassing early-return + // optimizations) when we hit ComposeAttribute. + void ToggleForceCompositing() { mForceCompositing = true; } + + // Transfers |aOther|'s mCachedBaseValue to |this| + void StealCachedBaseValue(SMILCompositor* aOther) { + mCachedBaseValue = std::move(aOther->mCachedBaseValue); + } + + private: + // Create a SMILAttr for my target, on the heap. + // + // @param aBaseComputedStyle An optional ComputedStyle which, if set, will be + // used when fetching the base style. + UniquePtr<SMILAttr> CreateSMILAttr(const ComputedStyle* aBaseComputedStyle); + + // Returns the CSS property this compositor should animate, or + // eCSSProperty_UNKNOWN if this compositor does not animate a CSS property. + nsCSSPropertyID GetCSSPropertyToAnimate() const; + + // Returns true if we might need to refer to base styles (i.e. we are + // targeting a CSS property and have one or more animation functions that + // don't just replace the underlying value). + // + // This might return true in some cases where we don't actually need the base + // style since it doesn't build up the animation sandwich to check if the + // functions that appear to need the base style are actually replaced by + // a function further up the stack. + bool MightNeedBaseStyle() const; + + // Finds the index of the first function that will affect our animation + // sandwich. Also toggles the 'mForceCompositing' flag if it finds that any + // (used) functions have changed. + uint32_t GetFirstFuncToAffectSandwich(); + + // If the passed-in base value differs from our cached base value, this + // method updates the cached value (and toggles the 'mForceCompositing' flag) + void UpdateCachedBaseValue(const SMILValue& aBaseValue); + + // The hash key (tuple of element and attributeName) + KeyType mKey; + + // Hash Value: List of animation functions that animate the specified attr + nsTArray<SMILAnimationFunction*> mAnimationFunctions; + + // Member data for detecting when we need to force-recompose + // --------------------------------------------------------- + // Flag for tracking whether we need to compose. Initialized to false, but + // gets flipped to true if we detect that something has changed. + bool mForceCompositing; + + // Cached base value, so we can detect & force-recompose when it changes + // from one sample to the next. (SMILAnimationController moves this + // forward from the previous sample's compositor by calling + // StealCachedBaseValue.) + SMILValue mCachedBaseValue; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILCOMPOSITOR_H_ diff --git a/dom/smil/SMILCompositorTable.h b/dom/smil/SMILCompositorTable.h new file mode 100644 index 0000000000..649c108872 --- /dev/null +++ b/dom/smil/SMILCompositorTable.h @@ -0,0 +1,28 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILCOMPOSITORTABLE_H_ +#define DOM_SMIL_SMILCOMPOSITORTABLE_H_ + +#include "nsTHashtable.h" + +//---------------------------------------------------------------------- +// SMILCompositorTable : A hashmap of SMILCompositors +// +// This is just a forward-declaration because it is included in +// SMILAnimationController which is used in Document. We don't want to +// expose all of SMILCompositor or otherwise any changes to it will mean the +// whole world will need to be rebuilt. + +namespace mozilla { + +class SMILCompositor; + +using SMILCompositorTable = nsTHashtable<SMILCompositor>; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILCOMPOSITORTABLE_H_ diff --git a/dom/smil/SMILEnumType.cpp b/dom/smil/SMILEnumType.cpp new file mode 100644 index 0000000000..fbc5c6d2b8 --- /dev/null +++ b/dom/smil/SMILEnumType.cpp @@ -0,0 +1,69 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILEnumType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include <math.h> + +namespace mozilla { + +void SMILEnumType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mUint = 0; + aValue.mType = this; +} + +void SMILEnumType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mUint = 0; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILEnumType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mUint = aSrc.mU.mUint; + return NS_OK; +} + +bool SMILEnumType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mUint == aRight.mU.mUint; +} + +nsresult SMILEnumType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // enum values can't be added to each other +} + +nsresult SMILEnumType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between enum + // values +} + +nsresult SMILEnumType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // enum values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILEnumType.h b/dom/smil/SMILEnumType.h new file mode 100644 index 0000000000..a380ad21b7 --- /dev/null +++ b/dom/smil/SMILEnumType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILENUMTYPE_H_ +#define DOM_SMIL_SMILENUMTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILEnumType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILEnumType* Singleton() { + static SMILEnumType sSingleton; + return &sSingleton; + } + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILEnumType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILENUMTYPE_H_ diff --git a/dom/smil/SMILFloatType.cpp b/dom/smil/SMILFloatType.cpp new file mode 100644 index 0000000000..ac50cfe8d2 --- /dev/null +++ b/dom/smil/SMILFloatType.cpp @@ -0,0 +1,81 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILFloatType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include <math.h> + +namespace mozilla { + +void SMILFloatType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mDouble = 0.0; + aValue.mType = this; +} + +void SMILFloatType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mDouble = 0.0; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILFloatType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mDouble = aSrc.mU.mDouble; + return NS_OK; +} + +bool SMILFloatType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mDouble == aRight.mU.mDouble; +} + +nsresult SMILFloatType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + aDest.mU.mDouble += aValueToAdd.mU.mDouble * aCount; + return NS_OK; +} + +nsresult SMILFloatType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + + const double& from = aFrom.mU.mDouble; + const double& to = aTo.mU.mDouble; + + aDistance = fabs(to - from); + + return NS_OK; +} + +nsresult SMILFloatType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + + const double& startVal = aStartVal.mU.mDouble; + const double& endVal = aEndVal.mU.mDouble; + + aResult.mU.mDouble = (startVal + (endVal - startVal) * aUnitDistance); + + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/smil/SMILFloatType.h b/dom/smil/SMILFloatType.h new file mode 100644 index 0000000000..18e3d0fdfa --- /dev/null +++ b/dom/smil/SMILFloatType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILFLOATTYPE_H_ +#define DOM_SMIL_SMILFLOATTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILFloatType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILFloatType* Singleton() { + static SMILFloatType sSingleton; + return &sSingleton; + } + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILFloatType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILFLOATTYPE_H_ diff --git a/dom/smil/SMILInstanceTime.cpp b/dom/smil/SMILInstanceTime.cpp new file mode 100644 index 0000000000..7f38b8c1d7 --- /dev/null +++ b/dom/smil/SMILInstanceTime.cpp @@ -0,0 +1,188 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILInstanceTime.h" + +#include "mozilla/AutoRestore.h" +#include "mozilla/SMILInterval.h" +#include "mozilla/SMILTimeValueSpec.h" + +namespace mozilla { + +//---------------------------------------------------------------------- +// Implementation + +SMILInstanceTime::SMILInstanceTime(const SMILTimeValue& aTime, + SMILInstanceTimeSource aSource, + SMILTimeValueSpec* aCreator, + SMILInterval* aBaseInterval) + : mTime(aTime), + mFlags(0), + mVisited(false), + mFixedEndpointRefCnt(0), + mSerial(0), + mCreator(aCreator), + mBaseInterval(nullptr) // This will get set to aBaseInterval in a call to + // SetBaseInterval() at end of constructor +{ + switch (aSource) { + case SOURCE_NONE: + // No special flags + break; + + case SOURCE_DOM: + mFlags = kDynamic | kFromDOM; + break; + + case SOURCE_SYNCBASE: + mFlags = kMayUpdate; + break; + + case SOURCE_EVENT: + mFlags = kDynamic; + break; + } + + SetBaseInterval(aBaseInterval); +} + +SMILInstanceTime::~SMILInstanceTime() { + MOZ_ASSERT(!mBaseInterval, + "Destroying instance time without first calling Unlink()"); + MOZ_ASSERT(mFixedEndpointRefCnt == 0, + "Destroying instance time that is still used as the fixed " + "endpoint of an interval"); +} + +void SMILInstanceTime::Unlink() { + RefPtr<SMILInstanceTime> deathGrip(this); + if (mBaseInterval) { + mBaseInterval->RemoveDependentTime(*this); + mBaseInterval = nullptr; + } + mCreator = nullptr; +} + +void SMILInstanceTime::HandleChangedInterval( + const SMILTimeContainer* aSrcContainer, bool aBeginObjectChanged, + bool aEndObjectChanged) { + // It's possible a sequence of notifications might cause our base interval to + // be updated and then deleted. Furthermore, the delete might happen whilst + // we're still in the queue to be notified of the change. In any case, if we + // don't have a base interval, just ignore the change. + if (!mBaseInterval) return; + + MOZ_ASSERT(mCreator, "Base interval is set but creator is not."); + + if (mVisited) { + // Break the cycle here + Unlink(); + return; + } + + bool objectChanged = + mCreator->DependsOnBegin() ? aBeginObjectChanged : aEndObjectChanged; + + RefPtr<SMILInstanceTime> deathGrip(this); + mozilla::AutoRestore<bool> setVisited(mVisited); + mVisited = true; + + mCreator->HandleChangedInstanceTime(*GetBaseTime(), aSrcContainer, *this, + objectChanged); +} + +void SMILInstanceTime::HandleDeletedInterval() { + MOZ_ASSERT(mBaseInterval, + "Got call to HandleDeletedInterval on an independent instance " + "time"); + MOZ_ASSERT(mCreator, "Base interval is set but creator is not"); + + mBaseInterval = nullptr; + mFlags &= ~kMayUpdate; // Can't update without a base interval + + RefPtr<SMILInstanceTime> deathGrip(this); + mCreator->HandleDeletedInstanceTime(*this); + mCreator = nullptr; +} + +void SMILInstanceTime::HandleFilteredInterval() { + MOZ_ASSERT(mBaseInterval, + "Got call to HandleFilteredInterval on an independent instance " + "time"); + + mBaseInterval = nullptr; + mFlags &= ~kMayUpdate; // Can't update without a base interval + mCreator = nullptr; +} + +bool SMILInstanceTime::ShouldPreserve() const { + return mFixedEndpointRefCnt > 0 || (mFlags & kWasDynamicEndpoint); +} + +void SMILInstanceTime::UnmarkShouldPreserve() { + mFlags &= ~kWasDynamicEndpoint; +} + +void SMILInstanceTime::AddRefFixedEndpoint() { + MOZ_ASSERT(mFixedEndpointRefCnt < UINT16_MAX, + "Fixed endpoint reference count upper limit reached"); + ++mFixedEndpointRefCnt; + mFlags &= ~kMayUpdate; // Once fixed, always fixed +} + +void SMILInstanceTime::ReleaseFixedEndpoint() { + MOZ_ASSERT(mFixedEndpointRefCnt > 0, "Duplicate release"); + --mFixedEndpointRefCnt; + if (mFixedEndpointRefCnt == 0 && IsDynamic()) { + mFlags |= kWasDynamicEndpoint; + } +} + +bool SMILInstanceTime::IsDependentOn(const SMILInstanceTime& aOther) const { + if (mVisited) return false; + + const SMILInstanceTime* myBaseTime = GetBaseTime(); + if (!myBaseTime) return false; + + if (myBaseTime == &aOther) return true; + + mozilla::AutoRestore<bool> setVisited(mVisited); + mVisited = true; + return myBaseTime->IsDependentOn(aOther); +} + +const SMILInstanceTime* SMILInstanceTime::GetBaseTime() const { + if (!mBaseInterval) { + return nullptr; + } + + MOZ_ASSERT(mCreator, "Base interval is set but there is no creator."); + if (!mCreator) { + return nullptr; + } + + return mCreator->DependsOnBegin() ? mBaseInterval->Begin() + : mBaseInterval->End(); +} + +void SMILInstanceTime::SetBaseInterval(SMILInterval* aBaseInterval) { + MOZ_ASSERT(!mBaseInterval, + "Attempting to reassociate an instance time with a different " + "interval."); + + if (aBaseInterval) { + MOZ_ASSERT(mCreator, + "Attempting to create a dependent instance time without " + "reference to the creating SMILTimeValueSpec object."); + if (!mCreator) return; + + aBaseInterval->AddDependentTime(*this); + } + + mBaseInterval = aBaseInterval; +} + +} // namespace mozilla diff --git a/dom/smil/SMILInstanceTime.h b/dom/smil/SMILInstanceTime.h new file mode 100644 index 0000000000..1224cfc8fe --- /dev/null +++ b/dom/smil/SMILInstanceTime.h @@ -0,0 +1,166 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILINSTANCETIME_H_ +#define DOM_SMIL_SMILINSTANCETIME_H_ + +#include "nsISupportsImpl.h" +#include "mozilla/SMILTimeValue.h" + +namespace mozilla { +class SMILInterval; +class SMILTimeContainer; +class SMILTimeValueSpec; + +//---------------------------------------------------------------------- +// SMILInstanceTime +// +// An instant in document simple time that may be used in creating a new +// interval. +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in SMILTimeValue.h +// +// These objects are owned by an SMILTimedElement but MAY also be referenced +// by: +// +// a) SMILIntervals that belong to the same SMILTimedElement and which refer +// to the SMILInstanceTimes which form the interval endpoints; and/or +// b) SMILIntervals that belong to other SMILTimedElements but which need to +// update dependent instance times when they change or are deleted. +// E.g. for begin='a.begin', 'a' needs to inform dependent +// SMILInstanceTimes if its begin time changes. This notification is +// performed by the SMILInterval. + +class SMILInstanceTime final { + public: + // Instance time source. Times generated by events, syncbase relationships, + // and DOM calls behave differently in some circumstances such as when a timed + // element is reset. + enum SMILInstanceTimeSource { + // No particularly significant source, e.g. offset time, 'indefinite' + SOURCE_NONE, + // Generated by a DOM call such as beginElement + SOURCE_DOM, + // Generated by a syncbase relationship + SOURCE_SYNCBASE, + // Generated by an event + SOURCE_EVENT + }; + + explicit SMILInstanceTime(const SMILTimeValue& aTime, + SMILInstanceTimeSource aSource = SOURCE_NONE, + SMILTimeValueSpec* aCreator = nullptr, + SMILInterval* aBaseInterval = nullptr); + + void Unlink(); + void HandleChangedInterval(const SMILTimeContainer* aSrcContainer, + bool aBeginObjectChanged, bool aEndObjectChanged); + void HandleDeletedInterval(); + void HandleFilteredInterval(); + + const SMILTimeValue& Time() const { return mTime; } + const SMILTimeValueSpec* GetCreator() const { return mCreator; } + + bool IsDynamic() const { return !!(mFlags & kDynamic); } + bool IsFixedTime() const { return !(mFlags & kMayUpdate); } + bool FromDOM() const { return !!(mFlags & kFromDOM); } + + bool ShouldPreserve() const; + void UnmarkShouldPreserve(); + + void AddRefFixedEndpoint(); + void ReleaseFixedEndpoint(); + + void DependentUpdate(const SMILTimeValue& aNewTime) { + MOZ_ASSERT(!IsFixedTime(), + "Updating an instance time that is not expected to be updated"); + mTime = aNewTime; + } + + bool IsDependent() const { return !!mBaseInterval; } + bool IsDependentOn(const SMILInstanceTime& aOther) const; + const SMILInterval* GetBaseInterval() const { return mBaseInterval; } + const SMILInstanceTime* GetBaseTime() const; + + bool SameTimeAndBase(const SMILInstanceTime& aOther) const { + return mTime == aOther.mTime && GetBaseTime() == aOther.GetBaseTime(); + } + + // Get and set a serial number which may be used by a containing class to + // control the sort order of otherwise similar instance times. + uint32_t Serial() const { return mSerial; } + void SetSerial(uint32_t aIndex) { mSerial = aIndex; } + + NS_INLINE_DECL_REFCOUNTING(SMILInstanceTime) + + private: + // Private destructor, to discourage deletion outside of Release(): + ~SMILInstanceTime(); + + void SetBaseInterval(SMILInterval* aBaseInterval); + + SMILTimeValue mTime; + + // Internal flags used to represent the behaviour of different instance times + enum { + // Indicates that this instance time was generated by an event or a DOM + // call. Such instance times require special handling when (i) the owning + // element is reset, (ii) when they are to be added as a new end instance + // times (as per SMIL's event sensitivity contraints), and (iii) when + // a backwards seek is performed and the timing model is reconstructed. + kDynamic = 1, + + // Indicates that this instance time is referred to by an + // SMILTimeValueSpec and as such may be updated. Such instance time should + // not be filtered out by the SMILTimedElement even if they appear to be + // in the past as they may be updated to a future time. + kMayUpdate = 2, + + // Indicates that this instance time was generated from the DOM as opposed + // to an SMILTimeValueSpec. When a 'begin' or 'end' attribute is set or + // reset we should clear all the instance times that have been generated by + // that attribute (and hence an SMILTimeValueSpec), but not those from the + // DOM. + kFromDOM = 4, + + // Indicates that this instance time was used as the endpoint of an interval + // that has been filtered or removed. However, since it is a dynamic time it + // should be preserved and not filtered. + kWasDynamicEndpoint = 8 + }; + uint8_t mFlags; // Combination of kDynamic, kMayUpdate, etc. + mutable bool mVisited; // Cycle tracking + + // Additional reference count to determine if this instance time is currently + // used as a fixed endpoint in any intervals. Instance times that are used in + // this way should not be removed when the owning SMILTimedElement removes + // instance times in response to a restart or in an attempt to free up memory + // by filtering out old instance times. + // + // Instance times are only shared in a few cases, namely: + // a) early ends, + // b) zero-duration intervals, + // c) momentarily whilst establishing new intervals and updating the current + // interval, and + // d) trimmed intervals + // Hence the limited range of a uint16_t should be more than adequate. + uint16_t mFixedEndpointRefCnt; + + uint32_t mSerial; // A serial number used by the containing class to + // specify the sort order for instance times with the + // same mTime. + + SMILTimeValueSpec* mCreator; // The SMILTimeValueSpec object that created + // us. (currently only needed for syncbase + // instance times.) + SMILInterval* mBaseInterval; // Interval from which this time is derived + // (only used for syncbase instance times) +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILINSTANCETIME_H_ diff --git a/dom/smil/SMILIntegerType.cpp b/dom/smil/SMILIntegerType.cpp new file mode 100644 index 0000000000..c1ac207836 --- /dev/null +++ b/dom/smil/SMILIntegerType.cpp @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILIntegerType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include <math.h> + +namespace mozilla { + +void SMILIntegerType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mInt = 0; + aValue.mType = this; +} + +void SMILIntegerType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + aValue.mU.mInt = 0; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILIntegerType::Assign(SMILValue& aDest, + const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + aDest.mU.mInt = aSrc.mU.mInt; + return NS_OK; +} + +bool SMILIntegerType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return aLeft.mU.mInt == aRight.mU.mInt; +} + +nsresult SMILIntegerType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + aDest.mU.mInt += aValueToAdd.mU.mInt * aCount; + return NS_OK; +} + +nsresult SMILIntegerType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + aDistance = fabs(double(aTo.mU.mInt - aFrom.mU.mInt)); + return NS_OK; +} + +nsresult SMILIntegerType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + + const double startVal = double(aStartVal.mU.mInt); + const double endVal = double(aEndVal.mU.mInt); + const double currentVal = startVal + (endVal - startVal) * aUnitDistance; + + // When currentVal is exactly midway between its two nearest integers, we + // jump to the "next" integer to provide simple, easy to remember and + // consistent behaviour (from the SMIL author's point of view). + + if (startVal < endVal) { + aResult.mU.mInt = int64_t(floor(currentVal + 0.5)); // round mid up + } else { + aResult.mU.mInt = int64_t(ceil(currentVal - 0.5)); // round mid down + } + + return NS_OK; +} + +} // namespace mozilla diff --git a/dom/smil/SMILIntegerType.h b/dom/smil/SMILIntegerType.h new file mode 100644 index 0000000000..6bdff63d80 --- /dev/null +++ b/dom/smil/SMILIntegerType.h @@ -0,0 +1,39 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILINTEGERTYPE_H_ +#define DOM_SMIL_SMILINTEGERTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILIntegerType : public SMILType { + public: + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + static SMILIntegerType* Singleton() { + static SMILIntegerType sSingleton; + return &sSingleton; + } + + private: + constexpr SMILIntegerType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILINTEGERTYPE_H_ diff --git a/dom/smil/SMILInterval.cpp b/dom/smil/SMILInterval.cpp new file mode 100644 index 0000000000..e0ee99ac00 --- /dev/null +++ b/dom/smil/SMILInterval.cpp @@ -0,0 +1,137 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILInterval.h" + +#include "mozilla/DebugOnly.h" + +namespace mozilla { + +SMILInterval::SMILInterval() : mBeginFixed(false), mEndFixed(false) {} + +SMILInterval::SMILInterval(const SMILInterval& aOther) + : mBegin(aOther.mBegin), + mEnd(aOther.mEnd), + mBeginFixed(false), + mEndFixed(false) { + MOZ_ASSERT(aOther.mDependentTimes.IsEmpty(), + "Attempt to copy-construct an interval with dependent times; this " + "will lead to instance times being shared between intervals."); + + // For the time being we don't allow intervals with fixed endpoints to be + // copied since we only ever copy-construct to establish a new current + // interval. If we ever need to copy historical intervals we may need to move + // the ReleaseFixedEndpoint calls from Unlink to the dtor. + MOZ_ASSERT(!aOther.mBeginFixed && !aOther.mEndFixed, + "Attempt to copy-construct an interval with fixed endpoints"); +} + +SMILInterval::~SMILInterval() { + MOZ_ASSERT(mDependentTimes.IsEmpty(), + "Destroying interval without disassociating dependent instance " + "times. Unlink was not called"); +} + +void SMILInterval::Unlink(bool aFiltered) { + for (int32_t i = mDependentTimes.Length() - 1; i >= 0; --i) { + if (aFiltered) { + mDependentTimes[i]->HandleFilteredInterval(); + } else { + mDependentTimes[i]->HandleDeletedInterval(); + } + } + mDependentTimes.Clear(); + if (mBegin && mBeginFixed) { + mBegin->ReleaseFixedEndpoint(); + } + mBegin = nullptr; + if (mEnd && mEndFixed) { + mEnd->ReleaseFixedEndpoint(); + } + mEnd = nullptr; +} + +SMILInstanceTime* SMILInterval::Begin() { + MOZ_ASSERT(mBegin && mEnd, "Requesting Begin() on un-initialized interval."); + return mBegin; +} + +SMILInstanceTime* SMILInterval::End() { + MOZ_ASSERT(mBegin && mEnd, "Requesting End() on un-initialized interval."); + return mEnd; +} + +void SMILInterval::SetBegin(SMILInstanceTime& aBegin) { + MOZ_ASSERT(aBegin.Time().IsDefinite(), + "Attempt to set unresolved or indefinite begin time on interval"); + MOZ_ASSERT(!mBeginFixed, + "Attempt to set begin time but the begin point is fixed"); + // Check that we're not making an instance time dependent on itself. Such an + // arrangement does not make intuitive sense and should be detected when + // creating or updating intervals. + MOZ_ASSERT(!mBegin || aBegin.GetBaseTime() != mBegin, + "Attempt to make self-dependent instance time"); + + mBegin = &aBegin; +} + +void SMILInterval::SetEnd(SMILInstanceTime& aEnd) { + MOZ_ASSERT(!mEndFixed, "Attempt to set end time but the end point is fixed"); + // As with SetBegin, check we're not making an instance time dependent on + // itself. + MOZ_ASSERT(!mEnd || aEnd.GetBaseTime() != mEnd, + "Attempting to make self-dependent instance time"); + + mEnd = &aEnd; +} + +void SMILInterval::FixBegin() { + MOZ_ASSERT(mBegin && mEnd, "Fixing begin point on un-initialized interval"); + MOZ_ASSERT(!mBeginFixed, "Duplicate calls to FixBegin()"); + mBeginFixed = true; + mBegin->AddRefFixedEndpoint(); +} + +void SMILInterval::FixEnd() { + MOZ_ASSERT(mBegin && mEnd, "Fixing end point on un-initialized interval"); + MOZ_ASSERT(mBeginFixed, + "Fixing the end of an interval without a fixed begin"); + MOZ_ASSERT(!mEndFixed, "Duplicate calls to FixEnd()"); + mEndFixed = true; + mEnd->AddRefFixedEndpoint(); +} + +void SMILInterval::AddDependentTime(SMILInstanceTime& aTime) { + RefPtr<SMILInstanceTime>* inserted = + mDependentTimes.InsertElementSorted(&aTime); + if (!inserted) { + NS_WARNING("Insufficient memory to insert instance time."); + } +} + +void SMILInterval::RemoveDependentTime(const SMILInstanceTime& aTime) { + DebugOnly<bool> found = mDependentTimes.RemoveElementSorted(&aTime); + MOZ_ASSERT(found, "Couldn't find instance time to delete."); +} + +void SMILInterval::GetDependentTimes(InstanceTimeList& aTimes) { + aTimes = mDependentTimes.Clone(); +} + +bool SMILInterval::IsDependencyChainLink() const { + if (!mBegin || !mEnd) + return false; // Not yet initialised so it can't be part of a chain + + if (mDependentTimes.IsEmpty()) return false; // No dependents, chain end + + // So we have dependents, but we're still only a link in the chain (as opposed + // to the end of the chain) if one of our endpoints is dependent on an + // interval other than ourselves. + return (mBegin->IsDependent() && mBegin->GetBaseInterval() != this) || + (mEnd->IsDependent() && mEnd->GetBaseInterval() != this); +} + +} // namespace mozilla diff --git a/dom/smil/SMILInterval.h b/dom/smil/SMILInterval.h new file mode 100644 index 0000000000..a569fce147 --- /dev/null +++ b/dom/smil/SMILInterval.h @@ -0,0 +1,86 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILINTERVAL_H_ +#define DOM_SMIL_SMILINTERVAL_H_ + +#include "mozilla/SMILInstanceTime.h" +#include "nsTArray.h" + +namespace mozilla { + +//---------------------------------------------------------------------- +// SMILInterval class +// +// A structure consisting of a begin and end time. The begin time must be +// resolved (i.e. not indefinite or unresolved). +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in SMILTimeValue.h + +class SMILInterval { + public: + SMILInterval(); + SMILInterval(const SMILInterval& aOther); + ~SMILInterval(); + void Unlink(bool aFiltered = false); + + const SMILInstanceTime* Begin() const { + MOZ_ASSERT(mBegin && mEnd, + "Requesting Begin() on un-initialized instance time"); + return mBegin; + } + SMILInstanceTime* Begin(); + + const SMILInstanceTime* End() const { + MOZ_ASSERT(mBegin && mEnd, + "Requesting End() on un-initialized instance time"); + return mEnd; + } + SMILInstanceTime* End(); + + void SetBegin(SMILInstanceTime& aBegin); + void SetEnd(SMILInstanceTime& aEnd); + void Set(SMILInstanceTime& aBegin, SMILInstanceTime& aEnd) { + SetBegin(aBegin); + SetEnd(aEnd); + } + + void FixBegin(); + void FixEnd(); + + using InstanceTimeList = nsTArray<RefPtr<SMILInstanceTime>>; + + void AddDependentTime(SMILInstanceTime& aTime); + void RemoveDependentTime(const SMILInstanceTime& aTime); + void GetDependentTimes(InstanceTimeList& aTimes); + + // Cue for assessing if this interval can be filtered + bool IsDependencyChainLink() const; + + private: + RefPtr<SMILInstanceTime> mBegin; + RefPtr<SMILInstanceTime> mEnd; + + // SMILInstanceTimes to notify when this interval is changed or deleted. + InstanceTimeList mDependentTimes; + + // Indicates if the end points of the interval are fixed or not. + // + // Note that this is not the same as having an end point whose TIME is fixed + // (i.e. SMILInstanceTime::IsFixed() returns true). This is because it is + // possible to have an end point with a fixed TIME and yet still update the + // end point to refer to a different SMILInstanceTime object. + // + // However, if mBegin/EndFixed is true, then BOTH the SMILInstanceTime + // OBJECT returned for that end point and its TIME value will not change. + bool mBeginFixed; + bool mEndFixed; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILINTERVAL_H_ diff --git a/dom/smil/SMILKeySpline.cpp b/dom/smil/SMILKeySpline.cpp new file mode 100644 index 0000000000..dd508e1bcb --- /dev/null +++ b/dom/smil/SMILKeySpline.cpp @@ -0,0 +1,127 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILKeySpline.h" +#include <stdint.h> +#include <math.h> + +namespace mozilla { + +#define NEWTON_ITERATIONS 4 +#define NEWTON_MIN_SLOPE 0.02 +#define SUBDIVISION_PRECISION 0.0000001 +#define SUBDIVISION_MAX_ITERATIONS 10 + +const double SMILKeySpline::kSampleStepSize = + 1.0 / double(kSplineTableSize - 1); + +void SMILKeySpline::Init(double aX1, double aY1, double aX2, double aY2) { + mX1 = aX1; + mY1 = aY1; + mX2 = aX2; + mY2 = aY2; + + if (mX1 != mY1 || mX2 != mY2) CalcSampleValues(); +} + +double SMILKeySpline::GetSplineValue(double aX) const { + if (mX1 == mY1 && mX2 == mY2) return aX; + + return CalcBezier(GetTForX(aX), mY1, mY2); +} + +void SMILKeySpline::GetSplineDerivativeValues(double aX, double& aDX, + double& aDY) const { + double t = GetTForX(aX); + aDX = GetSlope(t, mX1, mX2); + aDY = GetSlope(t, mY1, mY2); +} + +void SMILKeySpline::CalcSampleValues() { + for (uint32_t i = 0; i < kSplineTableSize; ++i) { + mSampleValues[i] = CalcBezier(double(i) * kSampleStepSize, mX1, mX2); + } +} + +/*static*/ +double SMILKeySpline::CalcBezier(double aT, double aA1, double aA2) { + // use Horner's scheme to evaluate the Bezier polynomial + return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT; +} + +/*static*/ +double SMILKeySpline::GetSlope(double aT, double aA1, double aA2) { + return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1); +} + +double SMILKeySpline::GetTForX(double aX) const { + // Early return when aX == 1.0 to avoid floating-point inaccuracies. + if (aX == 1.0) { + return 1.0; + } + // Find interval where t lies + double intervalStart = 0.0; + const double* currentSample = &mSampleValues[1]; + const double* const lastSample = &mSampleValues[kSplineTableSize - 1]; + for (; currentSample != lastSample && *currentSample <= aX; ++currentSample) { + intervalStart += kSampleStepSize; + } + --currentSample; // t now lies between *currentSample and *currentSample+1 + + // Interpolate to provide an initial guess for t + double dist = (aX - *currentSample) / (*(currentSample + 1) - *currentSample); + double guessForT = intervalStart + dist * kSampleStepSize; + + // Check the slope to see what strategy to use. If the slope is too small + // Newton-Raphson iteration won't converge on a root so we use bisection + // instead. + double initialSlope = GetSlope(guessForT, mX1, mX2); + if (initialSlope >= NEWTON_MIN_SLOPE) { + return NewtonRaphsonIterate(aX, guessForT); + } + if (initialSlope == 0.0) { + return guessForT; + } + return BinarySubdivide(aX, intervalStart, intervalStart + kSampleStepSize); +} + +double SMILKeySpline::NewtonRaphsonIterate(double aX, double aGuessT) const { + // Refine guess with Newton-Raphson iteration + for (uint32_t i = 0; i < NEWTON_ITERATIONS; ++i) { + // We're trying to find where f(t) = aX, + // so we're actually looking for a root for: CalcBezier(t) - aX + double currentX = CalcBezier(aGuessT, mX1, mX2) - aX; + double currentSlope = GetSlope(aGuessT, mX1, mX2); + + if (currentSlope == 0.0) return aGuessT; + + aGuessT -= currentX / currentSlope; + } + + return aGuessT; +} + +double SMILKeySpline::BinarySubdivide(double aX, double aA, double aB) const { + double currentX; + double currentT; + uint32_t i = 0; + + do { + currentT = aA + (aB - aA) / 2.0; + currentX = CalcBezier(currentT, mX1, mX2) - aX; + + if (currentX > 0.0) { + aB = currentT; + } else { + aA = currentT; + } + } while (fabs(currentX) > SUBDIVISION_PRECISION && + ++i < SUBDIVISION_MAX_ITERATIONS); + + return currentT; +} + +} // namespace mozilla diff --git a/dom/smil/SMILKeySpline.h b/dom/smil/SMILKeySpline.h new file mode 100644 index 0000000000..d5f180e94a --- /dev/null +++ b/dom/smil/SMILKeySpline.h @@ -0,0 +1,107 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILKEYSPLINE_H_ +#define DOM_SMIL_SMILKEYSPLINE_H_ + +#include "mozilla/ArrayUtils.h" +#include "mozilla/PodOperations.h" + +namespace mozilla { + +/** + * Utility class to provide scaling defined in a keySplines element. + */ +class SMILKeySpline { + public: + constexpr SMILKeySpline() : mX1(0), mY1(0), mX2(0), mY2(0) { + /* caller must call Init later */\ + } + + /** + * Creates a new key spline control point description. + * + * aX1, etc. are the x1, y1, x2, y2 cubic Bezier control points as defined + * by SMILANIM 3.2.3. They must each be in the range 0.0 <= x <= 1.0 + */ + SMILKeySpline(double aX1, double aY1, double aX2, double aY2) + : mX1(0), mY1(0), mX2(0), mY2(0) { + Init(aX1, aY1, aX2, aY2); + } + + double X1() const { return mX1; } + double Y1() const { return mY1; } + double X2() const { return mX2; } + double Y2() const { return mY2; } + + void Init(double aX1, double aY1, double aX2, double aY2); + + /** + * Gets the output (y) value for an input (x). + * + * @param aX The input x value. A floating-point number between 0 and + * 1 (inclusive). + */ + double GetSplineValue(double aX) const; + + void GetSplineDerivativeValues(double aX, double& aDX, double& aDY) const; + + bool operator==(const SMILKeySpline& aOther) const { + return mX1 == aOther.mX1 && mY1 == aOther.mY1 && mX2 == aOther.mX2 && + mY2 == aOther.mY2; + } + bool operator!=(const SMILKeySpline& aOther) const { + return !(*this == aOther); + } + int32_t Compare(const SMILKeySpline& aRhs) const { + if (mX1 != aRhs.mX1) return mX1 < aRhs.mX1 ? -1 : 1; + if (mY1 != aRhs.mY1) return mY1 < aRhs.mY1 ? -1 : 1; + if (mX2 != aRhs.mX2) return mX2 < aRhs.mX2 ? -1 : 1; + if (mY2 != aRhs.mY2) return mY2 < aRhs.mY2 ? -1 : 1; + return 0; + } + + private: + void CalcSampleValues(); + + /** + * Returns x(t) given t, x1, and x2, or y(t) given t, y1, and y2. + */ + static double CalcBezier(double aT, double aA1, double aA2); + + /** + * Returns dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2. + */ + static double GetSlope(double aT, double aA1, double aA2); + + double GetTForX(double aX) const; + + double NewtonRaphsonIterate(double aX, double aGuessT) const; + + double BinarySubdivide(double aX, double aA, double aB) const; + + static double A(double aA1, double aA2) { + return 1.0 - 3.0 * aA2 + 3.0 * aA1; + } + + static double B(double aA1, double aA2) { return 3.0 * aA2 - 6.0 * aA1; } + + static double C(double aA1) { return 3.0 * aA1; } + + double mX1; + double mY1; + double mX2; + double mY2; + + enum { kSplineTableSize = 11 }; + double mSampleValues[kSplineTableSize] = {}; + + static const double kSampleStepSize; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILKEYSPLINE_H_ diff --git a/dom/smil/SMILMilestone.h b/dom/smil/SMILMilestone.h new file mode 100644 index 0000000000..8b9372428f --- /dev/null +++ b/dom/smil/SMILMilestone.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILMILESTONE_H_ +#define DOM_SMIL_SMILMILESTONE_H_ + +#include "mozilla/SMILTypes.h" + +namespace mozilla { + +/* + * A significant moment in an SMILTimedElement's lifetime where a sample is + * required. + * + * Animations register the next milestone in their lifetime with the time + * container that they belong to. When the animation controller goes to run + * a sample it first visits all the animations that have a registered milestone + * in order of their milestone times. This allows interdependencies between + * animations to be correctly resolved and events to fire in the proper order. + * + * A distinction is made between a milestone representing the end of an interval + * and any other milestone. This is because if animation A ends at time t, and + * animation B begins at the same time t (or has some other significant moment + * such as firing a repeat event), SMIL's endpoint-exclusive timing model + * implies that the interval end occurs first. In fact, interval ends can be + * thought of as ending an infinitesimally small time before t. Therefore, + * A should be sampled before B. + * + * Furthermore, this distinction between sampling the end of an interval and + * a regular sample is used within the timing model (specifically in + * SMILTimedElement) to ensure that all intervals ending at time t are sampled + * before any new intervals are entered so that we have a fully up-to-date set + * of instance times available before committing to a new interval. Once an + * interval is entered, the begin time is fixed. + */ +class SMILMilestone { + public: + SMILMilestone(SMILTime aTime, bool aIsEnd) : mTime(aTime), mIsEnd(aIsEnd) {} + + SMILMilestone() : mTime(0), mIsEnd(false) {} + + bool operator==(const SMILMilestone& aOther) const { + return mTime == aOther.mTime && mIsEnd == aOther.mIsEnd; + } + + bool operator!=(const SMILMilestone& aOther) const { + return !(*this == aOther); + } + + bool operator<(const SMILMilestone& aOther) const { + // Earlier times sort first, and for equal times end milestones sort first + return mTime < aOther.mTime || + (mTime == aOther.mTime && mIsEnd && !aOther.mIsEnd); + } + + bool operator<=(const SMILMilestone& aOther) const { + return *this == aOther || *this < aOther; + } + + bool operator>=(const SMILMilestone& aOther) const { + return !(*this < aOther); + } + + SMILTime mTime; // The milestone time. This may be in container time or + // parent container time depending on where it is used. + bool mIsEnd; // true if this milestone corresponds to an interval + // end, false otherwise. +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILMILESTONE_H_ diff --git a/dom/smil/SMILNullType.cpp b/dom/smil/SMILNullType.cpp new file mode 100644 index 0000000000..571f6f3afe --- /dev/null +++ b/dom/smil/SMILNullType.cpp @@ -0,0 +1,57 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILNullType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" + +namespace mozilla { + +/*static*/ +SMILNullType* SMILNullType::Singleton() { + static SMILNullType sSingleton; + return &sSingleton; +} + +nsresult SMILNullType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aSrc.mType == this, "Unexpected source type"); + aDest.mU = aSrc.mU; + aDest.mType = Singleton(); + return NS_OK; +} + +bool SMILNullType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + return true; // All null-typed values are equivalent. +} + +nsresult SMILNullType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT_UNREACHABLE("Adding NULL type"); + return NS_ERROR_FAILURE; +} + +nsresult SMILNullType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT_UNREACHABLE("Computing distance for NULL type"); + return NS_ERROR_FAILURE; +} + +nsresult SMILNullType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT_UNREACHABLE("Interpolating NULL type"); + return NS_ERROR_FAILURE; +} + +} // namespace mozilla diff --git a/dom/smil/SMILNullType.h b/dom/smil/SMILNullType.h new file mode 100644 index 0000000000..83f69b2478 --- /dev/null +++ b/dom/smil/SMILNullType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILNULLTYPE_H_ +#define DOM_SMIL_SMILNULLTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILNullType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILNullType* Singleton(); + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override {} + void Destroy(SMILValue& aValue) const override {} + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + + // The remaining methods should never be called, so although they're very + // simple they don't need to be inline. + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILNullType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILNULLTYPE_H_ diff --git a/dom/smil/SMILParserUtils.cpp b/dom/smil/SMILParserUtils.cpp new file mode 100644 index 0000000000..fa994f6613 --- /dev/null +++ b/dom/smil/SMILParserUtils.cpp @@ -0,0 +1,633 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILParserUtils.h" + +#include "mozilla/SMILAttr.h" +#include "mozilla/SMILKeySpline.h" +#include "mozilla/SMILRepeatCount.h" +#include "mozilla/SMILTimeValue.h" +#include "mozilla/SMILTimeValueSpecParams.h" +#include "mozilla/SMILTypes.h" +#include "mozilla/SMILValue.h" +#include "mozilla/SVGContentUtils.h" +#include "mozilla/TextUtils.h" +#include "nsContentUtils.h" +#include "nsCharSeparatedTokenizer.h" + +using namespace mozilla::dom; +//------------------------------------------------------------------------------ +// Helper functions and Constants + +namespace { + +using namespace mozilla; + +const uint32_t MSEC_PER_SEC = 1000; +const uint32_t MSEC_PER_MIN = 1000 * 60; +const uint32_t MSEC_PER_HOUR = 1000 * 60 * 60; + +#define ACCESSKEY_PREFIX_LC u"accesskey("_ns // SMIL2+ +#define ACCESSKEY_PREFIX_CC u"accessKey("_ns // SVG/SMIL ANIM +#define REPEAT_PREFIX u"repeat("_ns +#define WALLCLOCK_PREFIX u"wallclock("_ns + +inline bool SkipWhitespace(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd) { + while (aIter != aEnd) { + if (!nsContentUtils::IsHTMLWhitespace(*aIter)) { + return true; + } + ++aIter; + } + return false; +} + +inline bool ParseColon(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd) { + if (aIter == aEnd || *aIter != ':') { + return false; + } + ++aIter; + return true; +} + +/* + * Exactly two digits in the range 00 - 59 are expected. + */ +bool ParseSecondsOrMinutes(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + uint32_t& aValue) { + if (aIter == aEnd || !mozilla::IsAsciiDigit(*aIter)) { + return false; + } + + RangedPtr<const char16_t> iter(aIter); + + if (++iter == aEnd || !mozilla::IsAsciiDigit(*iter)) { + return false; + } + + uint32_t value = 10 * mozilla::AsciiAlphanumericToNumber(*aIter) + + mozilla::AsciiAlphanumericToNumber(*iter); + if (value > 59) { + return false; + } + if (++iter != aEnd && mozilla::IsAsciiDigit(*iter)) { + return false; + } + + aValue = value; + aIter = iter; + return true; +} + +inline bool ParseClockMetric(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + uint32_t& aMultiplier) { + if (aIter == aEnd) { + aMultiplier = MSEC_PER_SEC; + return true; + } + + switch (*aIter) { + case 'h': + if (++aIter == aEnd) { + aMultiplier = MSEC_PER_HOUR; + return true; + } + return false; + case 'm': { + const nsAString& metric = Substring(aIter.get(), aEnd.get()); + if (metric.EqualsLiteral("min")) { + aMultiplier = MSEC_PER_MIN; + aIter = aEnd; + return true; + } + if (metric.EqualsLiteral("ms")) { + aMultiplier = 1; + aIter = aEnd; + return true; + } + } + return false; + case 's': + if (++aIter == aEnd) { + aMultiplier = MSEC_PER_SEC; + return true; + } + } + return false; +} + +/** + * See http://www.w3.org/TR/SVG/animate.html#ClockValueSyntax + */ +bool ParseClockValue(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + SMILTimeValue* aResult) { + if (aIter == aEnd) { + return false; + } + + // TIMECOUNT_VALUE ::= Timecount ("." Fraction)? (Metric)? + // PARTIAL_CLOCK_VALUE ::= Minutes ":" Seconds ("." Fraction)? + // FULL_CLOCK_VALUE ::= Hours ":" Minutes ":" Seconds ("." Fraction)? + enum ClockType { TIMECOUNT_VALUE, PARTIAL_CLOCK_VALUE, FULL_CLOCK_VALUE }; + + int32_t clockType = TIMECOUNT_VALUE; + + RangedPtr<const char16_t> iter(aIter); + + // Determine which type of clock value we have by counting the number + // of colons in the string. + do { + switch (*iter) { + case ':': + if (clockType == FULL_CLOCK_VALUE) { + return false; + } + ++clockType; + break; + case 'e': + case 'E': + case '-': + case '+': + // Exclude anything invalid (for clock values) + // that number parsing might otherwise allow. + return false; + } + ++iter; + } while (iter != aEnd); + + iter = aIter; + + int32_t hours = 0, timecount; + double fraction = 0.0; + uint32_t minutes, seconds, multiplier; + + switch (clockType) { + case FULL_CLOCK_VALUE: + if (!SVGContentUtils::ParseInteger(iter, aEnd, hours) || + !ParseColon(iter, aEnd)) { + return false; + } + [[fallthrough]]; + case PARTIAL_CLOCK_VALUE: + if (!ParseSecondsOrMinutes(iter, aEnd, minutes) || + !ParseColon(iter, aEnd) || + !ParseSecondsOrMinutes(iter, aEnd, seconds)) { + return false; + } + if (iter != aEnd && (*iter != '.' || !SVGContentUtils::ParseNumber( + iter, aEnd, fraction))) { + return false; + } + aResult->SetMillis(SMILTime(hours) * MSEC_PER_HOUR + + minutes * MSEC_PER_MIN + seconds * MSEC_PER_SEC + + NS_round(fraction * MSEC_PER_SEC)); + aIter = iter; + return true; + case TIMECOUNT_VALUE: + if (!SVGContentUtils::ParseInteger(iter, aEnd, timecount)) { + return false; + } + if (iter != aEnd && *iter == '.' && + !SVGContentUtils::ParseNumber(iter, aEnd, fraction)) { + return false; + } + if (!ParseClockMetric(iter, aEnd, multiplier)) { + return false; + } + aResult->SetMillis(SMILTime(timecount) * multiplier + + NS_round(fraction * multiplier)); + aIter = iter; + return true; + } + + return false; +} + +bool ParseOffsetValue(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + SMILTimeValue* aResult) { + RangedPtr<const char16_t> iter(aIter); + + int32_t sign; + if (!SVGContentUtils::ParseOptionalSign(iter, aEnd, sign) || + !SkipWhitespace(iter, aEnd) || !ParseClockValue(iter, aEnd, aResult)) { + return false; + } + if (sign == -1) { + aResult->SetMillis(-aResult->GetMillis()); + } + aIter = iter; + return true; +} + +bool ParseOffsetValue(const nsAString& aSpec, SMILTimeValue* aResult) { + RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + const RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + return ParseOffsetValue(iter, end, aResult) && iter == end; +} + +bool ParseOptionalOffset(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, + SMILTimeValue* aResult) { + if (aIter == aEnd) { + aResult->SetMillis(0L); + return true; + } + + return SkipWhitespace(aIter, aEnd) && ParseOffsetValue(aIter, aEnd, aResult); +} + +void MoveToNextToken(RangedPtr<const char16_t>& aIter, + const RangedPtr<const char16_t>& aEnd, bool aBreakOnDot, + bool& aIsAnyCharEscaped) { + aIsAnyCharEscaped = false; + + bool isCurrentCharEscaped = false; + + while (aIter != aEnd && !nsContentUtils::IsHTMLWhitespace(*aIter)) { + if (isCurrentCharEscaped) { + isCurrentCharEscaped = false; + } else { + if (*aIter == '+' || *aIter == '-' || (aBreakOnDot && *aIter == '.')) { + break; + } + if (*aIter == '\\') { + isCurrentCharEscaped = true; + aIsAnyCharEscaped = true; + } + } + ++aIter; + } +} + +already_AddRefed<nsAtom> ConvertUnescapedTokenToAtom(const nsAString& aToken) { + // Whether the token is an id-ref or event-symbol it should be a valid NCName + if (aToken.IsEmpty() || NS_FAILED(nsContentUtils::CheckQName(aToken, false))) + return nullptr; + return NS_Atomize(aToken); +} + +already_AddRefed<nsAtom> ConvertTokenToAtom(const nsAString& aToken, + bool aUnescapeToken) { + // Unescaping involves making a copy of the string which we'd like to avoid if + // possible + if (!aUnescapeToken) { + return ConvertUnescapedTokenToAtom(aToken); + } + + nsAutoString token(aToken); + + const char16_t* read = token.BeginReading(); + const char16_t* const end = token.EndReading(); + char16_t* write = token.BeginWriting(); + bool escape = false; + + while (read != end) { + MOZ_ASSERT(write <= read, "Writing past where we've read"); + if (!escape && *read == '\\') { + escape = true; + ++read; + } else { + *write++ = *read++; + escape = false; + } + } + token.Truncate(write - token.BeginReading()); + + return ConvertUnescapedTokenToAtom(token); +} + +bool ParseElementBaseTimeValueSpec(const nsAString& aSpec, + SMILTimeValueSpecParams& aResult) { + SMILTimeValueSpecParams result; + + // + // The spec will probably look something like one of these + // + // element-name.begin + // element-name.event-name + // event-name + // element-name.repeat(3) + // event\.name + // + // Technically `repeat(3)' is permitted but the behaviour in this case is not + // defined (for SMIL Animation) so we don't support it here. + // + + RangedPtr<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + if (start == end) { + return false; + } + + RangedPtr<const char16_t> tokenEnd(start); + + bool requiresUnescaping; + MoveToNextToken(tokenEnd, end, true, requiresUnescaping); + + RefPtr<nsAtom> atom = ConvertTokenToAtom( + Substring(start.get(), tokenEnd.get()), requiresUnescaping); + if (atom == nullptr) { + return false; + } + + // Parse the second token if there is one + if (tokenEnd != end && *tokenEnd == '.') { + result.mDependentElemID = atom; + + ++tokenEnd; + start = tokenEnd; + MoveToNextToken(tokenEnd, end, false, requiresUnescaping); + + const nsAString& token2 = Substring(start.get(), tokenEnd.get()); + + // element-name.begin + if (token2.EqualsLiteral("begin")) { + result.mType = SMILTimeValueSpecParams::SYNCBASE; + result.mSyncBegin = true; + // element-name.end + } else if (token2.EqualsLiteral("end")) { + result.mType = SMILTimeValueSpecParams::SYNCBASE; + result.mSyncBegin = false; + // element-name.repeat(digit+) + } else if (StringBeginsWith(token2, REPEAT_PREFIX)) { + start += REPEAT_PREFIX.Length(); + int32_t repeatValue; + if (start == tokenEnd || *start == '+' || *start == '-' || + !SVGContentUtils::ParseInteger(start, tokenEnd, repeatValue)) { + return false; + } + if (start == tokenEnd || *start != ')') { + return false; + } + result.mType = SMILTimeValueSpecParams::REPEAT; + result.mRepeatIteration = repeatValue; + // element-name.event-symbol + } else { + atom = ConvertTokenToAtom(token2, requiresUnescaping); + if (atom == nullptr) { + return false; + } + result.mType = SMILTimeValueSpecParams::EVENT; + result.mEventSymbol = atom; + } + } else { + // event-symbol + result.mType = SMILTimeValueSpecParams::EVENT; + result.mEventSymbol = atom; + } + + // We've reached the end of the token, so we should now be either looking at + // a '+', '-' (possibly with whitespace before it), or the end. + if (!ParseOptionalOffset(tokenEnd, end, &result.mOffset) || tokenEnd != end) { + return false; + } + aResult = result; + return true; +} + +} // namespace + +namespace mozilla { + +//------------------------------------------------------------------------------ +// Implementation + +const nsDependentSubstring SMILParserUtils::TrimWhitespace( + const nsAString& aString) { + nsAString::const_iterator start, end; + + aString.BeginReading(start); + aString.EndReading(end); + + // Skip whitespace characters at the beginning + while (start != end && nsContentUtils::IsHTMLWhitespace(*start)) { + ++start; + } + + // Skip whitespace characters at the end. + while (end != start) { + --end; + + if (!nsContentUtils::IsHTMLWhitespace(*end)) { + // Step back to the last non-whitespace character. + ++end; + + break; + } + } + + return Substring(start, end); +} + +bool SMILParserUtils::ParseKeySplines( + const nsAString& aSpec, FallibleTArray<SMILKeySpline>& aKeySplines) { + for (const auto& controlPoint : + nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace>(aSpec, + ';') + .ToRange()) { + nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace, + nsTokenizerFlags::SeparatorOptional> + tokenizer(controlPoint, ','); + + double values[4]; + for (auto& value : values) { + if (!tokenizer.hasMoreTokens() || + !SVGContentUtils::ParseNumber(tokenizer.nextToken(), value) || + value > 1.0 || value < 0.0) { + return false; + } + } + if (tokenizer.hasMoreTokens() || tokenizer.separatorAfterCurrentToken() || + !aKeySplines.AppendElement( + SMILKeySpline(values[0], values[1], values[2], values[3]), + fallible)) { + return false; + } + } + + return !aKeySplines.IsEmpty(); +} + +bool SMILParserUtils::ParseSemicolonDelimitedProgressList( + const nsAString& aSpec, bool aNonDecreasing, + FallibleTArray<double>& aArray) { + nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tokenizer( + aSpec, ';'); + + double previousValue = -1.0; + + while (tokenizer.hasMoreTokens()) { + double value; + if (!SVGContentUtils::ParseNumber(tokenizer.nextToken(), value)) { + return false; + } + + if (value > 1.0 || value < 0.0 || + (aNonDecreasing && value < previousValue)) { + return false; + } + + if (!aArray.AppendElement(value, fallible)) { + return false; + } + previousValue = value; + } + + return !aArray.IsEmpty(); +} + +// Helper class for ParseValues +class MOZ_STACK_CLASS SMILValueParser + : public SMILParserUtils::GenericValueParser { + public: + SMILValueParser(const SVGAnimationElement* aSrcElement, + const SMILAttr* aSMILAttr, + FallibleTArray<SMILValue>* aValuesArray, + bool* aPreventCachingOfSandwich) + : mSrcElement(aSrcElement), + mSMILAttr(aSMILAttr), + mValuesArray(aValuesArray), + mPreventCachingOfSandwich(aPreventCachingOfSandwich) {} + + bool Parse(const nsAString& aValueStr) override { + SMILValue newValue; + bool tmpPreventCachingOfSandwich = false; + if (NS_FAILED(mSMILAttr->ValueFromString(aValueStr, mSrcElement, newValue, + tmpPreventCachingOfSandwich))) + return false; + + if (!mValuesArray->AppendElement(newValue, fallible)) { + return false; + } + if (tmpPreventCachingOfSandwich) { + *mPreventCachingOfSandwich = true; + } + return true; + } + + protected: + const SVGAnimationElement* mSrcElement; + const SMILAttr* mSMILAttr; + FallibleTArray<SMILValue>* mValuesArray; + bool* mPreventCachingOfSandwich; +}; + +bool SMILParserUtils::ParseValues(const nsAString& aSpec, + const SVGAnimationElement* aSrcElement, + const SMILAttr& aAttribute, + FallibleTArray<SMILValue>& aValuesArray, + bool& aPreventCachingOfSandwich) { + // Assume all results can be cached, until we find one that can't. + aPreventCachingOfSandwich = false; + SMILValueParser valueParser(aSrcElement, &aAttribute, &aValuesArray, + &aPreventCachingOfSandwich); + return ParseValuesGeneric(aSpec, valueParser); +} + +bool SMILParserUtils::ParseValuesGeneric(const nsAString& aSpec, + GenericValueParser& aParser) { + nsCharSeparatedTokenizerTemplate<nsContentUtils::IsHTMLWhitespace> tokenizer( + aSpec, ';'); + if (!tokenizer.hasMoreTokens()) { // Empty list + return false; + } + + while (tokenizer.hasMoreTokens()) { + if (!aParser.Parse(tokenizer.nextToken())) { + return false; + } + } + + return true; +} + +bool SMILParserUtils::ParseRepeatCount(const nsAString& aSpec, + SMILRepeatCount& aResult) { + const nsAString& spec = SMILParserUtils::TrimWhitespace(aSpec); + + if (spec.EqualsLiteral("indefinite")) { + aResult.SetIndefinite(); + return true; + } + + double value; + if (!SVGContentUtils::ParseNumber(spec, value) || value <= 0.0) { + return false; + } + aResult = value; + return true; +} + +bool SMILParserUtils::ParseTimeValueSpecParams( + const nsAString& aSpec, SMILTimeValueSpecParams& aResult) { + const nsAString& spec = TrimWhitespace(aSpec); + + if (spec.EqualsLiteral("indefinite")) { + aResult.mType = SMILTimeValueSpecParams::INDEFINITE; + return true; + } + + // offset type + if (ParseOffsetValue(spec, &aResult.mOffset)) { + aResult.mType = SMILTimeValueSpecParams::OFFSET; + return true; + } + + // wallclock type + if (StringBeginsWith(spec, WALLCLOCK_PREFIX)) { + return false; // Wallclock times not implemented + } + + // accesskey type + if (StringBeginsWith(spec, ACCESSKEY_PREFIX_LC) || + StringBeginsWith(spec, ACCESSKEY_PREFIX_CC)) { + return false; // accesskey is not supported + } + + // event, syncbase, or repeat + return ParseElementBaseTimeValueSpec(spec, aResult); +} + +bool SMILParserUtils::ParseClockValue(const nsAString& aSpec, + SMILTimeValue* aResult) { + RangedPtr<const char16_t> iter(SVGContentUtils::GetStartRangedPtr(aSpec)); + RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aSpec)); + + return ::ParseClockValue(iter, end, aResult) && iter == end; +} + +int32_t SMILParserUtils::CheckForNegativeNumber(const nsAString& aStr) { + int32_t absValLocation = -1; + + RangedPtr<const char16_t> start(SVGContentUtils::GetStartRangedPtr(aStr)); + RangedPtr<const char16_t> iter = start; + RangedPtr<const char16_t> end(SVGContentUtils::GetEndRangedPtr(aStr)); + + // Skip initial whitespace + while (iter != end && nsContentUtils::IsHTMLWhitespace(*iter)) { + ++iter; + } + + // Check for dash + if (iter != end && *iter == '-') { + ++iter; + // Check for numeric character + if (iter != end && mozilla::IsAsciiDigit(*iter)) { + absValLocation = iter - start; + } + } + return absValLocation; +} + +} // namespace mozilla diff --git a/dom/smil/SMILParserUtils.h b/dom/smil/SMILParserUtils.h new file mode 100644 index 0000000000..bc17df051f --- /dev/null +++ b/dom/smil/SMILParserUtils.h @@ -0,0 +1,89 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILPARSERUTILS_H_ +#define DOM_SMIL_SMILPARSERUTILS_H_ + +#include "nsTArray.h" +#include "nsStringFwd.h" + +namespace mozilla { + +class SMILAttr; +class SMILKeySpline; +class SMILRepeatCount; +class SMILTimeValue; +class SMILTimeValueSpecParams; +class SMILValue; + +namespace dom { +class SVGAnimationElement; +} // namespace dom + +/** + * Common parsing utilities for the SMIL module. There is little re-use here; it + * simply serves to simplify other classes by moving parsing outside and to aid + * unit testing. + */ +class SMILParserUtils { + public: + // Abstract helper-class for assisting in parsing |values| attribute + class MOZ_STACK_CLASS GenericValueParser { + public: + virtual bool Parse(const nsAString& aValueStr) = 0; + }; + + static const nsDependentSubstring TrimWhitespace(const nsAString& aString); + + static bool ParseKeySplines(const nsAString& aSpec, + FallibleTArray<SMILKeySpline>& aKeySplines); + + // Used for parsing the |keyTimes| and |keyPoints| attributes. + static bool ParseSemicolonDelimitedProgressList( + const nsAString& aSpec, bool aNonDecreasing, + FallibleTArray<double>& aArray); + + static bool ParseValues(const nsAString& aSpec, + const mozilla::dom::SVGAnimationElement* aSrcElement, + const SMILAttr& aAttribute, + FallibleTArray<SMILValue>& aValuesArray, + bool& aPreventCachingOfSandwich); + + // Generic method that will run some code on each sub-section of an animation + // element's "values" list. + static bool ParseValuesGeneric(const nsAString& aSpec, + GenericValueParser& aParser); + + static bool ParseRepeatCount(const nsAString& aSpec, + SMILRepeatCount& aResult); + + static bool ParseTimeValueSpecParams(const nsAString& aSpec, + SMILTimeValueSpecParams& aResult); + + /* + * Parses a clock value as defined in the SMIL Animation specification. + * If parsing succeeds the returned value will be a non-negative, definite + * time value i.e. IsDefinite will return true. + * + * @param aSpec The string containing a clock value, e.g. "10s" + * @param aResult The parsed result. [OUT] + * @return true if parsing succeeded, otherwise false. + */ + static bool ParseClockValue(const nsAString& aSpec, SMILTimeValue* aResult); + + /* + * This method checks whether the given string looks like a negative number. + * Specifically, it checks whether the string looks matches the pattern + * "[whitespace]*-[numeral].*" If the string matches this pattern, this + * method returns the index of the first character after the '-' sign + * (i.e. the index of the absolute value). If not, this method returns -1. + */ + static int32_t CheckForNegativeNumber(const nsAString& aStr); +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILPARSERUTILS_H_ diff --git a/dom/smil/SMILRepeatCount.cpp b/dom/smil/SMILRepeatCount.cpp new file mode 100644 index 0000000000..9a56a42d45 --- /dev/null +++ b/dom/smil/SMILRepeatCount.cpp @@ -0,0 +1,14 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILRepeatCount.h" + +namespace mozilla { + +/*static*/ const double SMILRepeatCount::kNotSet = -1.0; +/*static*/ const double SMILRepeatCount::kIndefinite = -2.0; + +} // namespace mozilla diff --git a/dom/smil/SMILRepeatCount.h b/dom/smil/SMILRepeatCount.h new file mode 100644 index 0000000000..0b93aae994 --- /dev/null +++ b/dom/smil/SMILRepeatCount.h @@ -0,0 +1,62 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILREPEATCOUNT_H_ +#define DOM_SMIL_SMILREPEATCOUNT_H_ + +#include "nsDebug.h" +#include <math.h> + +namespace mozilla { + +//---------------------------------------------------------------------- +// SMILRepeatCount +// +// A tri-state non-negative floating point number for representing the number of +// times an animation repeat, i.e. the SMIL repeatCount attribute. +// +// The three states are: +// 1. not-set +// 2. set (with non-negative, non-zero count value) +// 3. indefinite +// +class SMILRepeatCount { + public: + SMILRepeatCount() : mCount(kNotSet) {} + explicit SMILRepeatCount(double aCount) : mCount(kNotSet) { + SetCount(aCount); + } + + operator double() const { + MOZ_ASSERT(IsDefinite(), + "Converting indefinite or unset repeat count to double"); + return mCount; + } + bool IsDefinite() const { return mCount != kNotSet && mCount != kIndefinite; } + bool IsIndefinite() const { return mCount == kIndefinite; } + bool IsSet() const { return mCount != kNotSet; } + + SMILRepeatCount& operator=(double aCount) { + SetCount(aCount); + return *this; + } + void SetCount(double aCount) { + NS_ASSERTION(aCount > 0.0, "Negative or zero repeat count"); + mCount = aCount > 0.0 ? aCount : kNotSet; + } + void SetIndefinite() { mCount = kIndefinite; } + void Unset() { mCount = kNotSet; } + + private: + static const double kNotSet; + static const double kIndefinite; + + double mCount; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILREPEATCOUNT_H_ diff --git a/dom/smil/SMILSetAnimationFunction.cpp b/dom/smil/SMILSetAnimationFunction.cpp new file mode 100644 index 0000000000..38f3498c01 --- /dev/null +++ b/dom/smil/SMILSetAnimationFunction.cpp @@ -0,0 +1,79 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILSetAnimationFunction.h" + +namespace mozilla { + +inline bool SMILSetAnimationFunction::IsDisallowedAttribute( + const nsAtom* aAttribute) const { + // + // A <set> element is similar to <animate> but lacks: + // AnimationValue.attrib(calcMode, values, keyTimes, keySplines, from, to, + // by) -- BUT has 'to' + // AnimationAddition.attrib(additive, accumulate) + // + return aAttribute == nsGkAtoms::calcMode || aAttribute == nsGkAtoms::values || + aAttribute == nsGkAtoms::keyTimes || + aAttribute == nsGkAtoms::keySplines || aAttribute == nsGkAtoms::from || + aAttribute == nsGkAtoms::by || aAttribute == nsGkAtoms::additive || + aAttribute == nsGkAtoms::accumulate; +} + +bool SMILSetAnimationFunction::SetAttr(nsAtom* aAttribute, + const nsAString& aValue, + nsAttrValue& aResult, + nsresult* aParseResult) { + if (IsDisallowedAttribute(aAttribute)) { + aResult.SetTo(aValue); + if (aParseResult) { + // SMILANIM 4.2 says: + // + // The additive and accumulate attributes are not allowed, and will be + // ignored if specified. + // + // So at least for those two attributes we shouldn't report an error even + // if they're present. For now we'll also just silently ignore other + // attribute types too. + *aParseResult = NS_OK; + } + return true; + } + + return SMILAnimationFunction::SetAttr(aAttribute, aValue, aResult, + aParseResult); +} + +bool SMILSetAnimationFunction::UnsetAttr(nsAtom* aAttribute) { + if (IsDisallowedAttribute(aAttribute)) { + return true; + } + + return SMILAnimationFunction::UnsetAttr(aAttribute); +} + +bool SMILSetAnimationFunction::HasAttr(nsAtom* aAttName) const { + if (IsDisallowedAttribute(aAttName)) return false; + + return SMILAnimationFunction::HasAttr(aAttName); +} + +const nsAttrValue* SMILSetAnimationFunction::GetAttr(nsAtom* aAttName) const { + if (IsDisallowedAttribute(aAttName)) return nullptr; + + return SMILAnimationFunction::GetAttr(aAttName); +} + +bool SMILSetAnimationFunction::GetAttr(nsAtom* aAttName, + nsAString& aResult) const { + if (IsDisallowedAttribute(aAttName)) return false; + + return SMILAnimationFunction::GetAttr(aAttName, aResult); +} + +bool SMILSetAnimationFunction::WillReplace() const { return true; } + +} // namespace mozilla diff --git a/dom/smil/SMILSetAnimationFunction.h b/dom/smil/SMILSetAnimationFunction.h new file mode 100644 index 0000000000..24dd2f695f --- /dev/null +++ b/dom/smil/SMILSetAnimationFunction.h @@ -0,0 +1,66 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILSETANIMATIONFUNCTION_H_ +#define DOM_SMIL_SMILSETANIMATIONFUNCTION_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILAnimationFunction.h" + +namespace mozilla { + +//---------------------------------------------------------------------- +// SMILSetAnimationFunction +// +// Subclass of SMILAnimationFunction that limits the behaviour to that offered +// by a <set> element. +// +class SMILSetAnimationFunction : public SMILAnimationFunction { + public: + /* + * Sets animation-specific attributes (or marks them dirty, in the case + * of from/to/by/values). + * + * @param aAttribute The attribute being set + * @param aValue The updated value of the attribute. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aParseResult Outparam used for reporting parse errors. Will be set + * to NS_OK if everything succeeds. + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + bool SetAttr(nsAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, nsresult* aParseResult = nullptr) override; + + /* + * Unsets the given attribute. + * + * @returns true if aAttribute is a recognized animation-related + * attribute; false otherwise. + */ + bool UnsetAttr(nsAtom* aAttribute) override; + + protected: + // Although <set> animation might look like to-animation, unlike to-animation, + // it never interpolates values. + // Returning false here will mean this animation function gets treated as + // a single-valued function and no interpolation will be attempted. + bool IsToAnimation() const override { return false; } + + // <set> applies the exact same value across the simple duration. + bool IsValueFixedForSimpleDuration() const override { return true; } + bool HasAttr(nsAtom* aAttName) const override; + const nsAttrValue* GetAttr(nsAtom* aAttName) const override; + bool GetAttr(nsAtom* aAttName, nsAString& aResult) const override; + bool WillReplace() const override; + + bool IsDisallowedAttribute(const nsAtom* aAttribute) const; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILSETANIMATIONFUNCTION_H_ diff --git a/dom/smil/SMILStringType.cpp b/dom/smil/SMILStringType.cpp new file mode 100644 index 0000000000..a79a97eb2c --- /dev/null +++ b/dom/smil/SMILStringType.cpp @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILStringType.h" + +#include "mozilla/SMILValue.h" +#include "nsDebug.h" +#include "nsString.h" + +namespace mozilla { + +void SMILStringType::Init(SMILValue& aValue) const { + MOZ_ASSERT(aValue.IsNull(), "Unexpected value type"); + aValue.mU.mPtr = new nsString(); + aValue.mType = this; +} + +void SMILStringType::Destroy(SMILValue& aValue) const { + MOZ_ASSERT(aValue.mType == this, "Unexpected SMIL value"); + delete static_cast<nsAString*>(aValue.mU.mPtr); + aValue.mU.mPtr = nullptr; + aValue.mType = SMILNullType::Singleton(); +} + +nsresult SMILStringType::Assign(SMILValue& aDest, const SMILValue& aSrc) const { + MOZ_ASSERT(aDest.mType == aSrc.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aDest.mType == this, "Unexpected SMIL value"); + + const nsAString* src = static_cast<const nsAString*>(aSrc.mU.mPtr); + nsAString* dst = static_cast<nsAString*>(aDest.mU.mPtr); + *dst = *src; + return NS_OK; +} + +bool SMILStringType::IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const { + MOZ_ASSERT(aLeft.mType == aRight.mType, "Incompatible SMIL types"); + MOZ_ASSERT(aLeft.mType == this, "Unexpected type for SMIL value"); + + const nsAString* leftString = static_cast<const nsAString*>(aLeft.mU.mPtr); + const nsAString* rightString = static_cast<nsAString*>(aRight.mU.mPtr); + return *leftString == *rightString; +} + +nsresult SMILStringType::Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const { + MOZ_ASSERT(aValueToAdd.mType == aDest.mType, "Trying to add invalid types"); + MOZ_ASSERT(aValueToAdd.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // string values can't be added to each other +} + +nsresult SMILStringType::ComputeDistance(const SMILValue& aFrom, + const SMILValue& aTo, + double& aDistance) const { + MOZ_ASSERT(aFrom.mType == aTo.mType, "Trying to compare different types"); + MOZ_ASSERT(aFrom.mType == this, "Unexpected source type"); + return NS_ERROR_FAILURE; // there is no concept of distance between string + // values +} + +nsresult SMILStringType::Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, + double aUnitDistance, + SMILValue& aResult) const { + MOZ_ASSERT(aStartVal.mType == aEndVal.mType, + "Trying to interpolate different types"); + MOZ_ASSERT(aStartVal.mType == this, "Unexpected types for interpolation"); + MOZ_ASSERT(aResult.mType == this, "Unexpected result type"); + return NS_ERROR_FAILURE; // string values do not interpolate +} + +} // namespace mozilla diff --git a/dom/smil/SMILStringType.h b/dom/smil/SMILStringType.h new file mode 100644 index 0000000000..7d33d6dade --- /dev/null +++ b/dom/smil/SMILStringType.h @@ -0,0 +1,44 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILSTRINGTYPE_H_ +#define DOM_SMIL_SMILSTRINGTYPE_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +class SMILStringType : public SMILType { + public: + // Singleton for SMILValue objects to hold onto. + static SMILStringType* Singleton() { + static SMILStringType sSingleton; + return &sSingleton; + } + + protected: + // SMILType Methods + // ------------------- + void Init(SMILValue& aValue) const override; + void Destroy(SMILValue& aValue) const override; + nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const override; + bool IsEqual(const SMILValue& aLeft, const SMILValue& aRight) const override; + nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const override; + nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const override; + nsresult Interpolate(const SMILValue& aStartVal, const SMILValue& aEndVal, + double aUnitDistance, SMILValue& aResult) const override; + + private: + // Private constructor: prevent instances beyond my singleton. + constexpr SMILStringType() = default; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILSTRINGTYPE_H_ diff --git a/dom/smil/SMILTargetIdentifier.h b/dom/smil/SMILTargetIdentifier.h new file mode 100644 index 0000000000..14a2ee3152 --- /dev/null +++ b/dom/smil/SMILTargetIdentifier.h @@ -0,0 +1,87 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTARGETIDENTIFIER_H_ +#define DOM_SMIL_SMILTARGETIDENTIFIER_H_ + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "nsAtom.h" +#include "nsIContent.h" +#include "mozilla/dom/Element.h" + +class nsIContent; + +namespace mozilla { +namespace dom { +class Element; +} + +/** + * Struct: SMILTargetIdentifier + * + * Tuple of: { Animated Element, Attribute Name } + * + * Used in SMILAnimationController as hash key for mapping an animation + * target to the SMILCompositor for that target. + * + * NOTE: Need a nsRefPtr for the element & attribute name, because + * SMILAnimationController retain its hash table for one sample into the + * future, and we need to make sure their target isn't deleted in that time. + */ + +struct SMILTargetIdentifier { + SMILTargetIdentifier() + : mElement(nullptr), + mAttributeName(nullptr), + mAttributeNamespaceID(kNameSpaceID_Unknown) {} + + inline bool Equals(const SMILTargetIdentifier& aOther) const { + return (aOther.mElement == mElement && + aOther.mAttributeName == mAttributeName && + aOther.mAttributeNamespaceID == mAttributeNamespaceID); + } + + RefPtr<mozilla::dom::Element> mElement; + RefPtr<nsAtom> mAttributeName; + int32_t mAttributeNamespaceID; +}; + +/** + * Class: SMILWeakTargetIdentifier + * + * Version of the above struct that uses non-owning pointers. These are kept + * private, to ensure that they aren't ever dereferenced (or used at all, + * outside of Equals()). + * + * This is solely for comparisons to determine if a target has changed + * from one sample to the next. + */ +class SMILWeakTargetIdentifier { + public: + // Trivial constructor + SMILWeakTargetIdentifier() : mElement(nullptr), mAttributeName(nullptr) {} + + // Allow us to update a weak identifier to match a given non-weak identifier + SMILWeakTargetIdentifier& operator=(const SMILTargetIdentifier& aOther) { + mElement = aOther.mElement; + mAttributeName = aOther.mAttributeName; + return *this; + } + + // Allow for comparison vs. non-weak identifier + inline bool Equals(const SMILTargetIdentifier& aOther) const { + return (aOther.mElement == mElement && + aOther.mAttributeName == mAttributeName); + } + + private: + const nsIContent* mElement; + const nsAtom* mAttributeName; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTARGETIDENTIFIER_H_ diff --git a/dom/smil/SMILTimeContainer.cpp b/dom/smil/SMILTimeContainer.cpp new file mode 100644 index 0000000000..321c27ed2c --- /dev/null +++ b/dom/smil/SMILTimeContainer.cpp @@ -0,0 +1,306 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILTimeContainer.h" + +#include "mozilla/AutoRestore.h" +#include "mozilla/CheckedInt.h" +#include "mozilla/SMILTimedElement.h" +#include "mozilla/SMILTimeValue.h" +#include <algorithm> + +namespace mozilla { + +SMILTimeContainer::SMILTimeContainer() + : mParent(nullptr), + mCurrentTime(0L), + mParentOffset(0L), + mPauseStart(0L), + mNeedsPauseSample(false), + mNeedsRewind(false), + mIsSeeking(false), +#ifdef DEBUG + mHoldingEntries(false), +#endif + mPauseState(PAUSE_BEGIN) { +} + +SMILTimeContainer::~SMILTimeContainer() { + if (mParent) { + mParent->RemoveChild(*this); + } +} + +SMILTimeValue SMILTimeContainer::ContainerToParentTime( + SMILTime aContainerTime) const { + // If we're paused, then future times are indefinite + if (IsPaused() && aContainerTime > mCurrentTime) + return SMILTimeValue::Indefinite(); + + return SMILTimeValue(aContainerTime + mParentOffset); +} + +SMILTimeValue SMILTimeContainer::ParentToContainerTime( + SMILTime aParentTime) const { + // If we're paused, then any time after when we paused is indefinite + if (IsPaused() && aParentTime > mPauseStart) + return SMILTimeValue::Indefinite(); + + return SMILTimeValue(aParentTime - mParentOffset); +} + +void SMILTimeContainer::Begin() { + Resume(PAUSE_BEGIN); + if (mPauseState) { + mNeedsPauseSample = true; + } + + // This is a little bit complicated here. Ideally we'd just like to call + // Sample() and force an initial sample but this turns out to be a bad idea + // because this may mean that NeedsSample() no longer reports true and so when + // we come to the first real sample our parent will skip us over altogether. + // So we force the time to be updated and adopt the policy to never call + // Sample() ourselves but to always leave that to our parent or client. + + UpdateCurrentTime(); +} + +void SMILTimeContainer::Pause(uint32_t aType) { + bool didStartPause = false; + + if (!mPauseState && aType) { + mPauseStart = GetParentTime(); + mNeedsPauseSample = true; + didStartPause = true; + } + + mPauseState |= aType; + + if (didStartPause) { + NotifyTimeChange(); + } +} + +void SMILTimeContainer::Resume(uint32_t aType) { + if (!mPauseState) return; + + mPauseState &= ~aType; + + if (!mPauseState) { + SMILTime extraOffset = GetParentTime() - mPauseStart; + mParentOffset += extraOffset; + NotifyTimeChange(); + } +} + +SMILTime SMILTimeContainer::GetCurrentTimeAsSMILTime() const { + // The following behaviour is consistent with: + // http://www.w3.org/2003/01/REC-SVG11-20030114-errata + // #getCurrentTime_setCurrentTime_undefined_before_document_timeline_begin + // which says that if GetCurrentTime is called before the document timeline + // has begun we should just return 0. + if (IsPausedByType(PAUSE_BEGIN)) return 0L; + + return mCurrentTime; +} + +void SMILTimeContainer::SetCurrentTime(SMILTime aSeekTo) { + // SVG 1.1 doesn't specify what to do for negative times so we adopt SVGT1.2's + // behaviour of clamping negative times to 0. + aSeekTo = std::max<SMILTime>(0, aSeekTo); + + // The following behaviour is consistent with: + // http://www.w3.org/2003/01/REC-SVG11-20030114-errata + // #getCurrentTime_setCurrentTime_undefined_before_document_timeline_begin + // which says that if SetCurrentTime is called before the document timeline + // has begun we should still adjust the offset. + SMILTime parentTime = GetParentTime(); + mParentOffset = parentTime - aSeekTo; + mIsSeeking = true; + + if (IsPaused()) { + mNeedsPauseSample = true; + mPauseStart = parentTime; + } + + if (aSeekTo < mCurrentTime) { + // Backwards seek + mNeedsRewind = true; + ClearMilestones(); + } + + // Force an update to the current time in case we get a call to GetCurrentTime + // before another call to Sample(). + UpdateCurrentTime(); + + NotifyTimeChange(); +} + +SMILTime SMILTimeContainer::GetParentTime() const { + if (mParent) return mParent->GetCurrentTimeAsSMILTime(); + + return 0L; +} + +void SMILTimeContainer::SyncPauseTime() { + if (IsPaused()) { + SMILTime parentTime = GetParentTime(); + SMILTime extraOffset = parentTime - mPauseStart; + mParentOffset += extraOffset; + mPauseStart = parentTime; + } +} + +void SMILTimeContainer::Sample() { + if (!NeedsSample()) return; + + UpdateCurrentTime(); + DoSample(); + + mNeedsPauseSample = false; +} + +nsresult SMILTimeContainer::SetParent(SMILTimeContainer* aParent) { + if (mParent) { + mParent->RemoveChild(*this); + // When we're not attached to a parent time container, GetParentTime() will + // return 0. We need to adjust our pause state information to be relative to + // this new time base. + // Note that since "current time = parent time - parent offset" setting the + // parent offset and pause start as follows preserves our current time even + // while parent time = 0. + mParentOffset = -mCurrentTime; + mPauseStart = 0L; + } + + mParent = aParent; + + nsresult rv = NS_OK; + if (mParent) { + rv = mParent->AddChild(*this); + } + + return rv; +} + +void SMILTimeContainer::AddMilestone( + const SMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement) { + // We record the milestone time and store it along with the element but this + // time may change (e.g. if attributes are changed on the timed element in + // between samples). If this happens, then we may do an unecessary sample + // but that's pretty cheap. + MOZ_ASSERT(!mHoldingEntries); + mMilestoneEntries.Push(MilestoneEntry(aMilestone, aElement)); +} + +void SMILTimeContainer::ClearMilestones() { + MOZ_ASSERT(!mHoldingEntries); + mMilestoneEntries.Clear(); +} + +bool SMILTimeContainer::GetNextMilestoneInParentTime( + SMILMilestone& aNextMilestone) const { + if (mMilestoneEntries.IsEmpty()) return false; + + SMILTimeValue parentTime = + ContainerToParentTime(mMilestoneEntries.Top().mMilestone.mTime); + if (!parentTime.IsDefinite()) return false; + + aNextMilestone = SMILMilestone(parentTime.GetMillis(), + mMilestoneEntries.Top().mMilestone.mIsEnd); + + return true; +} + +bool SMILTimeContainer::PopMilestoneElementsAtMilestone( + const SMILMilestone& aMilestone, AnimElemArray& aMatchedElements) { + if (mMilestoneEntries.IsEmpty()) return false; + + SMILTimeValue containerTime = ParentToContainerTime(aMilestone.mTime); + if (!containerTime.IsDefinite()) return false; + + SMILMilestone containerMilestone(containerTime.GetMillis(), + aMilestone.mIsEnd); + + MOZ_ASSERT(mMilestoneEntries.Top().mMilestone >= containerMilestone, + "Trying to pop off earliest times but we have earlier ones that " + "were overlooked"); + + MOZ_ASSERT(!mHoldingEntries); + + bool gotOne = false; + while (!mMilestoneEntries.IsEmpty() && + mMilestoneEntries.Top().mMilestone == containerMilestone) { + aMatchedElements.AppendElement(mMilestoneEntries.Pop().mTimebase); + gotOne = true; + } + + return gotOne; +} + +void SMILTimeContainer::Traverse( + nsCycleCollectionTraversalCallback* aCallback) { +#ifdef DEBUG + AutoRestore<bool> saveHolding(mHoldingEntries); + mHoldingEntries = true; +#endif + const MilestoneEntry* p = mMilestoneEntries.Elements(); + while (p < mMilestoneEntries.Elements() + mMilestoneEntries.Length()) { + NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(*aCallback, "mTimebase"); + aCallback->NoteXPCOMChild(static_cast<nsIContent*>(p->mTimebase.get())); + ++p; + } +} + +void SMILTimeContainer::Unlink() { + MOZ_ASSERT(!mHoldingEntries); + mMilestoneEntries.Clear(); +} + +void SMILTimeContainer::UpdateCurrentTime() { + SMILTime now = IsPaused() ? mPauseStart : GetParentTime(); + MOZ_ASSERT(now >= mParentOffset, + "Container has negative time with respect to parent"); + const auto updatedCurrentTime = CheckedInt<SMILTime>(now) - mParentOffset; + mCurrentTime = updatedCurrentTime.isValid() + ? updatedCurrentTime.value() + : std::numeric_limits<SMILTime>::max(); +} + +void SMILTimeContainer::NotifyTimeChange() { + // Called when the container time is changed with respect to the document + // time. When this happens time dependencies in other time containers need to + // re-resolve their times because begin and end times are stored in container + // time. + // + // To get the list of timed elements with dependencies we simply re-use the + // milestone elements. This is because any timed element with dependents and + // with significant transitions yet to fire should have their next milestone + // registered. Other timed elements don't matter. + + // Copy the timed elements to a separate array before calling + // HandleContainerTimeChange on each of them in case doing so mutates + // mMilestoneEntries. + nsTArray<RefPtr<mozilla::dom::SVGAnimationElement>> elems; + + { +#ifdef DEBUG + AutoRestore<bool> saveHolding(mHoldingEntries); + mHoldingEntries = true; +#endif + for (const MilestoneEntry* p = mMilestoneEntries.Elements(); + p < mMilestoneEntries.Elements() + mMilestoneEntries.Length(); ++p) { + elems.AppendElement(p->mTimebase.get()); + } + } + + for (auto& elem : elems) { + elem->TimedElement().HandleContainerTimeChange(); + } +} + +} // namespace mozilla diff --git a/dom/smil/SMILTimeContainer.h b/dom/smil/SMILTimeContainer.h new file mode 100644 index 0000000000..62ea7ad2c4 --- /dev/null +++ b/dom/smil/SMILTimeContainer.h @@ -0,0 +1,299 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMECONTAINER_H_ +#define DOM_SMIL_SMILTIMECONTAINER_H_ + +#include "mozilla/dom/SVGAnimationElement.h" +#include "mozilla/SMILMilestone.h" +#include "mozilla/SMILTypes.h" +#include "nscore.h" +#include "nsTPriorityQueue.h" + +namespace mozilla { + +class SMILTimeValue; + +//---------------------------------------------------------------------- +// SMILTimeContainer +// +// Common base class for a time base that can be paused, resumed, and sampled. +// +class SMILTimeContainer { + public: + SMILTimeContainer(); + virtual ~SMILTimeContainer(); + + /* + * Pause request types. + */ + enum { + PAUSE_BEGIN = 1, // Paused because timeline has yet to begin. + PAUSE_SCRIPT = 2, // Paused by script. + PAUSE_PAGEHIDE = 4, // Paused because our doc is hidden. + PAUSE_USERPREF = 8, // Paused because animations are disabled in prefs. + PAUSE_IMAGE = 16 // Paused becuase we're in an image that's suspended. + }; + + /* + * Cause the time container to record its begin time. + */ + void Begin(); + + /* + * Pause this time container + * + * @param aType The source of the pause request. Successive calls to Pause + * with the same aType will be ignored. The container will remain paused until + * each call to Pause of a given aType has been matched by at least one call + * to Resume with the same aType. + */ + virtual void Pause(uint32_t aType); + + /* + * Resume this time container + * + * param @aType The source of the resume request. Clears the pause flag for + * this particular type of pause request. When all pause flags have been + * cleared the time container will be resumed. + */ + virtual void Resume(uint32_t aType); + + /** + * Returns true if this time container is paused by the specified type. + * Note that the time container may also be paused by other types; this method + * does not test if aType is the exclusive pause source. + * + * @param @aType The pause source to test for. + * @return true if this container is paused by aType. + */ + bool IsPausedByType(uint32_t aType) const { return mPauseState & aType; } + + /** + * Returns true if this time container is paused. + * Generally you should test for a specific type of pausing using + * IsPausedByType. + * + * @return true if this container is paused, false otherwise. + */ + bool IsPaused() const { return mPauseState != 0; } + + /* + * Return the time elapsed since this time container's begin time (expressed + * in parent time) minus any accumulated offset from pausing. + */ + SMILTime GetCurrentTimeAsSMILTime() const; + + /* + * Seek the document timeline to the specified time. + * + * @param aSeekTo The time to seek to, expressed in this time container's time + * base (i.e. the same units as GetCurrentTime). + */ + void SetCurrentTime(SMILTime aSeekTo); + + /* + * Return the current time for the parent time container if any. + */ + virtual SMILTime GetParentTime() const; + + /* + * Convert container time to parent time. + * + * @param aContainerTime The container time to convert. + * @return The equivalent parent time or indefinite if the container is + * paused and the time is in the future. + */ + SMILTimeValue ContainerToParentTime(SMILTime aContainerTime) const; + + /* + * Convert from parent time to container time. + * + * @param aParentTime The parent time to convert. + * @return The equivalent container time or indefinite if the container is + * paused and aParentTime is after the time when the pause began. + */ + SMILTimeValue ParentToContainerTime(SMILTime aParentTime) const; + + /* + * If the container is paused, causes the pause time to be updated to the + * current parent time. This should be called before updating + * cross-container dependencies that will call ContainerToParentTime in order + * to provide more intuitive results. + */ + void SyncPauseTime(); + + /* + * Updates the current time of this time container and calls DoSample to + * perform any sample-operations. + */ + void Sample(); + + /* + * Return if this time container should be sampled or can be skipped. + * + * This is most useful as an optimisation for skipping time containers that + * don't require a sample. + */ + bool NeedsSample() const { return !mPauseState || mNeedsPauseSample; } + + /* + * Indicates if the elements of this time container need to be rewound. + * This occurs during a backwards seek. + */ + bool NeedsRewind() const { return mNeedsRewind; } + void ClearNeedsRewind() { mNeedsRewind = false; } + + /* + * Indicates the time container is currently processing a SetCurrentTime + * request and appropriate seek behaviour should be applied by child elements + * (e.g. not firing time events). + */ + bool IsSeeking() const { return mIsSeeking; } + void MarkSeekFinished() { mIsSeeking = false; } + + /* + * Sets the parent time container. + * + * The callee still retains ownership of the time container. + */ + nsresult SetParent(SMILTimeContainer* aParent); + + /* + * Registers an element for a sample at the given time. + * + * @param aMilestone The milestone to register in container time. + * @param aElement The timebase element that needs a sample at + * aMilestone. + */ + void AddMilestone(const SMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement); + + /* + * Resets the list of milestones. + */ + void ClearMilestones(); + + /* + * Returns the next significant transition from amongst the registered + * milestones. + * + * @param[out] aNextMilestone The next milestone with time in parent time. + * + * @return true if there exists another milestone, false otherwise in + * which case aNextMilestone will be unmodified. + */ + bool GetNextMilestoneInParentTime(SMILMilestone& aNextMilestone) const; + + using AnimElemArray = nsTArray<RefPtr<dom::SVGAnimationElement>>; + + /* + * Removes and returns the timebase elements from the start of the list of + * timebase elements that match the given time. + * + * @param aMilestone The milestone time to match in parent time. This + * must be <= GetNextMilestoneInParentTime. + * @param[out] aMatchedElements The array to which matching elements will be + * appended. + * @return true if one or more elements match, false otherwise. + */ + bool PopMilestoneElementsAtMilestone(const SMILMilestone& aMilestone, + AnimElemArray& aMatchedElements); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + protected: + /* + * Per-sample operations to be performed whenever Sample() is called and + * NeedsSample() is true. Called after updating mCurrentTime; + */ + virtual void DoSample() {} + + /* + * Adding and removing child containers is not implemented in the base class + * because not all subclasses need this. + */ + + /* + * Adds a child time container. + */ + virtual nsresult AddChild(SMILTimeContainer& aChild) { + return NS_ERROR_FAILURE; + } + + /* + * Removes a child time container. + */ + virtual void RemoveChild(SMILTimeContainer& aChild) {} + + /* + * Implementation helper to update the current time. + */ + void UpdateCurrentTime(); + + /* + * Implementation helper to notify timed elements with dependencies that the + * container time has changed with respect to the document time. + */ + void NotifyTimeChange(); + + // The parent time container, if any + SMILTimeContainer* mParent; + + // The current time established at the last call to Sample() + SMILTime mCurrentTime; + + // The number of milliseconds for which the container has been paused + // (excluding the current pause interval if the container is currently + // paused). + // + // Current time = parent time - mParentOffset + // + SMILTime mParentOffset; + + // The timestamp in parent time when the container was paused + SMILTime mPauseStart; + + // Whether or not a pause sample is required + bool mNeedsPauseSample; + + bool mNeedsRewind; // Backwards seek performed + bool mIsSeeking; // Currently in the middle of a seek operation + +#ifdef DEBUG + bool mHoldingEntries; // True if there's a raw pointer to mMilestoneEntries + // on the stack. +#endif + + // A bitfield of the pause state for all pause requests + uint32_t mPauseState; + + struct MilestoneEntry { + MilestoneEntry(const SMILMilestone& aMilestone, + mozilla::dom::SVGAnimationElement& aElement) + : mMilestone(aMilestone), mTimebase(&aElement) {} + + bool operator<(const MilestoneEntry& aOther) const { + return mMilestone < aOther.mMilestone; + } + + SMILMilestone mMilestone; // In container time. + RefPtr<mozilla::dom::SVGAnimationElement> mTimebase; + }; + + // Queue of elements with registered milestones. Used to update the model with + // significant transitions that occur between two samples. Since timed element + // re-register their milestones when they're sampled this is reset once we've + // taken care of the milestones before the current sample time but before we + // actually do the full sample. + nsTPriorityQueue<MilestoneEntry> mMilestoneEntries; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMECONTAINER_H_ diff --git a/dom/smil/SMILTimeValue.cpp b/dom/smil/SMILTimeValue.cpp new file mode 100644 index 0000000000..c198a74013 --- /dev/null +++ b/dom/smil/SMILTimeValue.cpp @@ -0,0 +1,42 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILTimeValue.h" + +namespace mozilla { + +const SMILTime SMILTimeValue::kUnresolvedMillis = + std::numeric_limits<SMILTime>::max(); + +//---------------------------------------------------------------------- +// SMILTimeValue methods: + +static inline int8_t Cmp(int64_t aA, int64_t aB) { + return aA == aB ? 0 : (aA > aB ? 1 : -1); +} + +int8_t SMILTimeValue::CompareTo(const SMILTimeValue& aOther) const { + int8_t result; + + if (mState == STATE_DEFINITE) { + result = (aOther.mState == STATE_DEFINITE) + ? Cmp(mMilliseconds, aOther.mMilliseconds) + : -1; + } else if (mState == STATE_INDEFINITE) { + if (aOther.mState == STATE_DEFINITE) + result = 1; + else if (aOther.mState == STATE_INDEFINITE) + result = 0; + else + result = -1; + } else { + result = (aOther.mState != STATE_UNRESOLVED) ? 1 : 0; + } + + return result; +} + +} // namespace mozilla diff --git a/dom/smil/SMILTimeValue.h b/dom/smil/SMILTimeValue.h new file mode 100644 index 0000000000..0fbe7d3b36 --- /dev/null +++ b/dom/smil/SMILTimeValue.h @@ -0,0 +1,131 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMEVALUE_H_ +#define DOM_SMIL_SMILTIMEVALUE_H_ + +#include "mozilla/SMILTypes.h" +#include "nsDebug.h" + +namespace mozilla { + +/*---------------------------------------------------------------------- + * SMILTimeValue class + * + * A tri-state time value. + * + * First a quick overview of the SMIL time data types: + * + * SMILTime -- a timestamp in milliseconds. + * SMILTimeValue -- (this class) a timestamp that can take the additional + * states 'indefinite' and 'unresolved' + * SMILInstanceTime -- an SMILTimeValue used for constructing intervals. It + * contains additional fields to govern reset behavior + * and track timing dependencies (e.g. syncbase timing). + * SMILInterval -- a pair of SMILInstanceTimes that defines a begin and + * an end time for animation. + * SMILTimeValueSpec -- a component of a begin or end attribute, such as the + * '5s' or 'a.end+2m' in begin="5s; a.end+2m". Acts as + * a broker between an SMILTimedElement and its + * SMILInstanceTimes by generating new instance times + * and handling changes to existing times. + * + * Objects of this class may be in one of three states: + * + * 1) The time is resolved and has a definite millisecond value + * 2) The time is resolved and indefinite + * 3) The time is unresolved + * + * In summary: + * + * State | GetMillis | IsDefinite | IsIndefinite | IsResolved + * -----------+---------------+------------+--------------+------------ + * Definite | SMILTimeValue | true | false | true + * -----------+---------------+------------+--------------+------------ + * Indefinite | -- | false | true | true + * -----------+---------------+------------+--------------+------------ + * Unresolved | -- | false | false | false + * + */ + +class SMILTimeValue { + public: + // Creates an unresolved time value + SMILTimeValue() + : mMilliseconds(kUnresolvedMillis), mState(STATE_UNRESOLVED) {} + + // Creates a resolved time value + explicit SMILTimeValue(SMILTime aMillis) + : mMilliseconds(aMillis), mState(STATE_DEFINITE) {} + + // Named constructor to create an indefinite time value + static SMILTimeValue Indefinite() { + SMILTimeValue value; + value.SetIndefinite(); + return value; + } + + bool IsIndefinite() const { return mState == STATE_INDEFINITE; } + void SetIndefinite() { + mState = STATE_INDEFINITE; + mMilliseconds = kUnresolvedMillis; + } + + bool IsResolved() const { return mState != STATE_UNRESOLVED; } + void SetUnresolved() { + mState = STATE_UNRESOLVED; + mMilliseconds = kUnresolvedMillis; + } + + bool IsDefinite() const { return mState == STATE_DEFINITE; } + SMILTime GetMillis() const { + MOZ_ASSERT(mState == STATE_DEFINITE, + "GetMillis() called for unresolved or indefinite time"); + + return mState == STATE_DEFINITE ? mMilliseconds : kUnresolvedMillis; + } + + void SetMillis(SMILTime aMillis) { + mState = STATE_DEFINITE; + mMilliseconds = aMillis; + } + + int8_t CompareTo(const SMILTimeValue& aOther) const; + + bool operator==(const SMILTimeValue& aOther) const { + return CompareTo(aOther) == 0; + } + + bool operator!=(const SMILTimeValue& aOther) const { + return CompareTo(aOther) != 0; + } + + bool operator<(const SMILTimeValue& aOther) const { + return CompareTo(aOther) < 0; + } + + bool operator>(const SMILTimeValue& aOther) const { + return CompareTo(aOther) > 0; + } + + bool operator<=(const SMILTimeValue& aOther) const { + return CompareTo(aOther) <= 0; + } + + bool operator>=(const SMILTimeValue& aOther) const { + return CompareTo(aOther) >= 0; + } + + private: + static const SMILTime kUnresolvedMillis; + + SMILTime mMilliseconds; + enum { STATE_DEFINITE, STATE_INDEFINITE, STATE_UNRESOLVED } mState; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMEVALUE_H_ diff --git a/dom/smil/SMILTimeValueSpec.cpp b/dom/smil/SMILTimeValueSpec.cpp new file mode 100644 index 0000000000..3ce58e9598 --- /dev/null +++ b/dom/smil/SMILTimeValueSpec.cpp @@ -0,0 +1,370 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/EventListenerManager.h" +#include "mozilla/SMILInstanceTime.h" +#include "mozilla/SMILInterval.h" +#include "mozilla/SMILParserUtils.h" +#include "mozilla/SMILTimeContainer.h" +#include "mozilla/SMILTimedElement.h" +#include "mozilla/SMILTimeValueSpec.h" +#include "mozilla/SMILTimeValue.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "mozilla/dom/TimeEvent.h" +#include "nsString.h" +#include <limits> + +using namespace mozilla::dom; + +namespace mozilla { + +//---------------------------------------------------------------------- +// Nested class: EventListener + +NS_IMPL_ISUPPORTS(SMILTimeValueSpec::EventListener, nsIDOMEventListener) + +NS_IMETHODIMP +SMILTimeValueSpec::EventListener::HandleEvent(Event* aEvent) { + if (mSpec) { + mSpec->HandleEvent(aEvent); + } + return NS_OK; +} + +//---------------------------------------------------------------------- +// Implementation + +SMILTimeValueSpec::SMILTimeValueSpec(SMILTimedElement& aOwner, bool aIsBegin) + : mOwner(&aOwner), mIsBegin(aIsBegin), mReferencedElement(this) {} + +SMILTimeValueSpec::~SMILTimeValueSpec() { + UnregisterFromReferencedElement(mReferencedElement.get()); + if (mEventListener) { + mEventListener->Disconnect(); + mEventListener = nullptr; + } +} + +nsresult SMILTimeValueSpec::SetSpec(const nsAString& aStringSpec, + Element& aContextElement) { + SMILTimeValueSpecParams params; + + if (!SMILParserUtils::ParseTimeValueSpecParams(aStringSpec, params)) + return NS_ERROR_FAILURE; + + mParams = params; + + // According to SMIL 3.0: + // The special value "indefinite" does not yield an instance time in the + // begin list. It will, however yield a single instance with the value + // "indefinite" in an end list. This value is not removed by a reset. + if (mParams.mType == SMILTimeValueSpecParams::OFFSET || + (!mIsBegin && mParams.mType == SMILTimeValueSpecParams::INDEFINITE)) { + mOwner->AddInstanceTime(new SMILInstanceTime(mParams.mOffset), mIsBegin); + } + + // Fill in the event symbol to simplify handling later + if (mParams.mType == SMILTimeValueSpecParams::REPEAT) { + mParams.mEventSymbol = nsGkAtoms::repeatEvent; + } + + ResolveReferences(aContextElement); + + return NS_OK; +} + +void SMILTimeValueSpec::ResolveReferences(Element& aContextElement) { + if (mParams.mType != SMILTimeValueSpecParams::SYNCBASE && !IsEventBased()) { + return; + } + + // If we're not bound to the document yet, don't worry, we'll get called again + // when that happens + if (!aContextElement.IsInComposedDoc()) return; + + // Hold ref to the old element so that it isn't destroyed in between resetting + // the referenced element and using the pointer to update the referenced + // element. + RefPtr<Element> oldReferencedElement = mReferencedElement.get(); + + if (mParams.mDependentElemID) { + mReferencedElement.ResetWithID(aContextElement, mParams.mDependentElemID); + } else if (mParams.mType == SMILTimeValueSpecParams::EVENT) { + Element* target = mOwner->GetTargetElement(); + mReferencedElement.ResetWithElement(target); + } else { + MOZ_ASSERT(false, "Syncbase or repeat spec without ID"); + } + UpdateReferencedElement(oldReferencedElement, mReferencedElement.get()); +} + +bool SMILTimeValueSpec::IsEventBased() const { + return mParams.mType == SMILTimeValueSpecParams::EVENT || + mParams.mType == SMILTimeValueSpecParams::REPEAT; +} + +void SMILTimeValueSpec::HandleNewInterval( + SMILInterval& aInterval, const SMILTimeContainer* aSrcContainer) { + const SMILInstanceTime& baseInstance = + mParams.mSyncBegin ? *aInterval.Begin() : *aInterval.End(); + SMILTimeValue newTime = + ConvertBetweenTimeContainers(baseInstance.Time(), aSrcContainer); + + // Apply offset + if (!ApplyOffset(newTime)) { + NS_WARNING("New time overflows SMILTime, ignoring"); + return; + } + + // Create the instance time and register it with the interval + RefPtr<SMILInstanceTime> newInstance = new SMILInstanceTime( + newTime, SMILInstanceTime::SOURCE_SYNCBASE, this, &aInterval); + mOwner->AddInstanceTime(newInstance, mIsBegin); +} + +void SMILTimeValueSpec::HandleTargetElementChange(Element* aNewTarget) { + if (!IsEventBased() || mParams.mDependentElemID) return; + + mReferencedElement.ResetWithElement(aNewTarget); +} + +void SMILTimeValueSpec::HandleChangedInstanceTime( + const SMILInstanceTime& aBaseTime, const SMILTimeContainer* aSrcContainer, + SMILInstanceTime& aInstanceTimeToUpdate, bool aObjectChanged) { + // If the instance time is fixed (e.g. because it's being used as the begin + // time of an active or postactive interval) we just ignore the change. + if (aInstanceTimeToUpdate.IsFixedTime()) return; + + SMILTimeValue updatedTime = + ConvertBetweenTimeContainers(aBaseTime.Time(), aSrcContainer); + + // Apply offset + if (!ApplyOffset(updatedTime)) { + NS_WARNING("Updated time overflows SMILTime, ignoring"); + return; + } + + // The timed element that owns the instance time does the updating so it can + // re-sort its array of instance times more efficiently + if (aInstanceTimeToUpdate.Time() != updatedTime || aObjectChanged) { + mOwner->UpdateInstanceTime(&aInstanceTimeToUpdate, updatedTime, mIsBegin); + } +} + +void SMILTimeValueSpec::HandleDeletedInstanceTime( + SMILInstanceTime& aInstanceTime) { + mOwner->RemoveInstanceTime(&aInstanceTime, mIsBegin); +} + +bool SMILTimeValueSpec::DependsOnBegin() const { return mParams.mSyncBegin; } + +void SMILTimeValueSpec::Traverse( + nsCycleCollectionTraversalCallback* aCallback) { + mReferencedElement.Traverse(aCallback); +} + +void SMILTimeValueSpec::Unlink() { + UnregisterFromReferencedElement(mReferencedElement.get()); + mReferencedElement.Unlink(); +} + +//---------------------------------------------------------------------- +// Implementation helpers + +void SMILTimeValueSpec::UpdateReferencedElement(Element* aFrom, Element* aTo) { + if (aFrom == aTo) return; + + UnregisterFromReferencedElement(aFrom); + + switch (mParams.mType) { + case SMILTimeValueSpecParams::SYNCBASE: { + SMILTimedElement* to = GetTimedElement(aTo); + if (to) { + to->AddDependent(*this); + } + } break; + + case SMILTimeValueSpecParams::EVENT: + case SMILTimeValueSpecParams::REPEAT: + RegisterEventListener(aTo); + break; + + default: + // not a referencing-type + break; + } +} + +void SMILTimeValueSpec::UnregisterFromReferencedElement(Element* aElement) { + if (!aElement) return; + + if (mParams.mType == SMILTimeValueSpecParams::SYNCBASE) { + SMILTimedElement* timedElement = GetTimedElement(aElement); + if (timedElement) { + timedElement->RemoveDependent(*this); + } + mOwner->RemoveInstanceTimesForCreator(this, mIsBegin); + } else if (IsEventBased()) { + UnregisterEventListener(aElement); + } +} + +SMILTimedElement* SMILTimeValueSpec::GetTimedElement(Element* aElement) { + nsCOMPtr<SVGAnimationElement> animationElement = do_QueryInterface(aElement); + return animationElement ? &animationElement->TimedElement() : nullptr; +} + +// Indicates whether we're allowed to register an event-listener +// when scripting is disabled. +bool SMILTimeValueSpec::IsWhitelistedEvent() { + // The category of (SMIL-specific) "repeat(n)" events are allowed. + if (mParams.mType == SMILTimeValueSpecParams::REPEAT) { + return true; + } + + // A specific list of other SMIL-related events are allowed, too. + if (mParams.mType == SMILTimeValueSpecParams::EVENT && + (mParams.mEventSymbol == nsGkAtoms::repeat || + mParams.mEventSymbol == nsGkAtoms::repeatEvent || + mParams.mEventSymbol == nsGkAtoms::beginEvent || + mParams.mEventSymbol == nsGkAtoms::endEvent)) { + return true; + } + + return false; +} + +void SMILTimeValueSpec::RegisterEventListener(Element* aTarget) { + MOZ_ASSERT(IsEventBased(), + "Attempting to register event-listener for unexpected " + "SMILTimeValueSpec type"); + MOZ_ASSERT(mParams.mEventSymbol, + "Attempting to register event-listener but there is no event " + "name"); + + if (!aTarget) return; + + // When script is disabled, only allow registration for whitelisted events. + if (!aTarget->GetOwnerDocument()->IsScriptEnabled() && + !IsWhitelistedEvent()) { + return; + } + + if (!mEventListener) { + mEventListener = new EventListener(this); + } + + EventListenerManager* elm = aTarget->GetOrCreateListenerManager(); + if (!elm) { + return; + } + + elm->AddEventListenerByType(mEventListener, + nsDependentAtomString(mParams.mEventSymbol), + AllEventsAtSystemGroupBubble()); +} + +void SMILTimeValueSpec::UnregisterEventListener(Element* aTarget) { + if (!aTarget || !mEventListener) { + return; + } + + EventListenerManager* elm = aTarget->GetOrCreateListenerManager(); + if (!elm) { + return; + } + + elm->RemoveEventListenerByType(mEventListener, + nsDependentAtomString(mParams.mEventSymbol), + AllEventsAtSystemGroupBubble()); +} + +void SMILTimeValueSpec::HandleEvent(Event* aEvent) { + MOZ_ASSERT(mEventListener, "Got event without an event listener"); + MOZ_ASSERT(IsEventBased(), "Got event for non-event SMILTimeValueSpec"); + MOZ_ASSERT(aEvent, "No event supplied"); + + // XXX In the long run we should get the time from the event itself which will + // store the time in global document time which we'll need to convert to our + // time container + SMILTimeContainer* container = mOwner->GetTimeContainer(); + if (!container) return; + + if (mParams.mType == SMILTimeValueSpecParams::REPEAT && + !CheckRepeatEventDetail(aEvent)) { + return; + } + + SMILTime currentTime = container->GetCurrentTimeAsSMILTime(); + SMILTimeValue newTime(currentTime); + if (!ApplyOffset(newTime)) { + NS_WARNING("New time generated from event overflows SMILTime, ignoring"); + return; + } + + RefPtr<SMILInstanceTime> newInstance = + new SMILInstanceTime(newTime, SMILInstanceTime::SOURCE_EVENT); + mOwner->AddInstanceTime(newInstance, mIsBegin); +} + +bool SMILTimeValueSpec::CheckRepeatEventDetail(Event* aEvent) { + TimeEvent* timeEvent = aEvent->AsTimeEvent(); + if (!timeEvent) { + NS_WARNING("Received a repeat event that was not a DOMTimeEvent"); + return false; + } + + int32_t detail = timeEvent->Detail(); + return detail > 0 && (uint32_t)detail == mParams.mRepeatIteration; +} + +SMILTimeValue SMILTimeValueSpec::ConvertBetweenTimeContainers( + const SMILTimeValue& aSrcTime, const SMILTimeContainer* aSrcContainer) { + // If the source time is either indefinite or unresolved the result is going + // to be the same + if (!aSrcTime.IsDefinite()) return aSrcTime; + + // Convert from source time container to our parent time container + const SMILTimeContainer* dstContainer = mOwner->GetTimeContainer(); + if (dstContainer == aSrcContainer) return aSrcTime; + + // If one of the elements is not attached to a time container then we can't do + // any meaningful conversion + if (!aSrcContainer || !dstContainer) return SMILTimeValue(); // unresolved + + SMILTimeValue docTime = + aSrcContainer->ContainerToParentTime(aSrcTime.GetMillis()); + + if (docTime.IsIndefinite()) + // This will happen if the source container is paused and we have a future + // time. Just return the indefinite time. + return docTime; + + MOZ_ASSERT(docTime.IsDefinite(), + "ContainerToParentTime gave us an unresolved or indefinite time"); + + return dstContainer->ParentToContainerTime(docTime.GetMillis()); +} + +bool SMILTimeValueSpec::ApplyOffset(SMILTimeValue& aTime) const { + // indefinite + offset = indefinite. Likewise for unresolved times. + if (!aTime.IsDefinite()) { + return true; + } + + double resultAsDouble = + (double)aTime.GetMillis() + mParams.mOffset.GetMillis(); + if (resultAsDouble > double(std::numeric_limits<SMILTime>::max()) || + resultAsDouble < double(std::numeric_limits<SMILTime>::min())) { + return false; + } + aTime.SetMillis(aTime.GetMillis() + mParams.mOffset.GetMillis()); + return true; +} + +} // namespace mozilla diff --git a/dom/smil/SMILTimeValueSpec.h b/dom/smil/SMILTimeValueSpec.h new file mode 100644 index 0000000000..4de81bc2b4 --- /dev/null +++ b/dom/smil/SMILTimeValueSpec.h @@ -0,0 +1,143 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMEVALUESPEC_H_ +#define DOM_SMIL_SMILTIMEVALUESPEC_H_ + +#include "mozilla/Attributes.h" +#include "mozilla/SMILTimeValueSpecParams.h" +#include "mozilla/dom/IDTracker.h" +#include "nsStringFwd.h" +#include "nsIDOMEventListener.h" + +// XXX Avoid including this here by moving function bodies to the cpp file +#include "mozilla/dom/Element.h" + +namespace mozilla { + +class EventListenerManager; +class SMILInstanceTime; +class SMILInterval; +class SMILTimeContainer; +class SMILTimedElement; +class SMILTimeValue; + +namespace dom { +class Event; +} // namespace dom + +//---------------------------------------------------------------------- +// SMILTimeValueSpec class +// +// An individual element of a 'begin' or 'end' attribute, e.g. '5s', 'a.end'. +// This class handles the parsing of such specifications and performs the +// necessary event handling (for event and repeat specifications) +// and synchronisation (for syncbase specifications). +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in SMILTimeValue.h + +class SMILTimeValueSpec { + public: + using Element = dom::Element; + using Event = dom::Event; + using IDTracker = dom::IDTracker; + + SMILTimeValueSpec(SMILTimedElement& aOwner, bool aIsBegin); + ~SMILTimeValueSpec(); + + nsresult SetSpec(const nsAString& aStringSpec, Element& aContextElement); + void ResolveReferences(Element& aContextElement); + bool IsEventBased() const; + + void HandleNewInterval(SMILInterval& aInterval, + const SMILTimeContainer* aSrcContainer); + void HandleTargetElementChange(Element* aNewTarget); + + // For created SMILInstanceTime objects + bool DependsOnBegin() const; + void HandleChangedInstanceTime(const SMILInstanceTime& aBaseTime, + const SMILTimeContainer* aSrcContainer, + SMILInstanceTime& aInstanceTimeToUpdate, + bool aObjectChanged); + void HandleDeletedInstanceTime(SMILInstanceTime& aInstanceTime); + + // Cycle-collection support + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + protected: + void UpdateReferencedElement(Element* aFrom, Element* aTo); + void UnregisterFromReferencedElement(Element* aElement); + SMILTimedElement* GetTimedElement(Element* aElement); + bool IsWhitelistedEvent(); + void RegisterEventListener(Element* aTarget); + void UnregisterEventListener(Element* aTarget); + void HandleEvent(Event* aEvent); + bool CheckRepeatEventDetail(Event* aEvent); + SMILTimeValue ConvertBetweenTimeContainers( + const SMILTimeValue& aSrcTime, const SMILTimeContainer* aSrcContainer); + bool ApplyOffset(SMILTimeValue& aTime) const; + + SMILTimedElement* mOwner; + bool mIsBegin; // Indicates if *we* are a begin spec, + // not to be confused with + // mParams.mSyncBegin which indicates + // if we're synced with the begin of + // the target. + SMILTimeValueSpecParams mParams; + + /** + * If our SMILTimeValueSpec exists for a 'begin' or 'end' attribute with a + * value that specifies a time that is relative to the animation of some + * other element, it will create an instance of this class to reference and + * track that other element. For example, if the SMILTimeValueSpec is for + * end='a.end+2s', an instance of this class will be created to track the + * element associated with the element ID "a". This class will notify the + * SMILTimeValueSpec if the element that that ID identifies changes to a + * different element (or none). + */ + class TimeReferenceTracker final : public IDTracker { + public: + explicit TimeReferenceTracker(SMILTimeValueSpec* aOwner) : mSpec(aOwner) {} + void ResetWithElement(Element* aTo) { + RefPtr<Element> from = get(); + Unlink(); + ElementChanged(from, aTo); + } + + protected: + void ElementChanged(Element* aFrom, Element* aTo) override { + IDTracker::ElementChanged(aFrom, aTo); + mSpec->UpdateReferencedElement(aFrom, aTo); + } + bool IsPersistent() override { return true; } + + private: + SMILTimeValueSpec* mSpec; + }; + + TimeReferenceTracker mReferencedElement; + + class EventListener final : public nsIDOMEventListener { + ~EventListener() {} + + public: + explicit EventListener(SMILTimeValueSpec* aOwner) : mSpec(aOwner) {} + void Disconnect() { mSpec = nullptr; } + + NS_DECL_ISUPPORTS + NS_DECL_NSIDOMEVENTLISTENER + + private: + SMILTimeValueSpec* mSpec; + }; + RefPtr<EventListener> mEventListener; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMEVALUESPEC_H_ diff --git a/dom/smil/SMILTimeValueSpecParams.h b/dom/smil/SMILTimeValueSpecParams.h new file mode 100644 index 0000000000..2487168b7f --- /dev/null +++ b/dom/smil/SMILTimeValueSpecParams.h @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMEVALUESPECPARAMS_H_ +#define DOM_SMIL_SMILTIMEVALUESPECPARAMS_H_ + +#include "mozilla/SMILTimeValue.h" +#include "nsAtom.h" + +namespace mozilla { + +//---------------------------------------------------------------------- +// SMILTimeValueSpecParams +// +// A simple data type for storing the result of parsing a single begin or end +// value (e.g. the '5s' in begin="5s; indefinite; a.begin+2s"). + +class SMILTimeValueSpecParams { + public: + SMILTimeValueSpecParams() + : mType(INDEFINITE), mSyncBegin(false), mRepeatIteration(0) {} + + // The type of value this specification describes + enum { OFFSET, SYNCBASE, EVENT, REPEAT, WALLCLOCK, INDEFINITE } mType; + + // A clock value that is added to: + // - type OFFSET: the document begin + // - type SYNCBASE: the timebase's begin or end time + // - type EVENT: the event time + // - type REPEAT: the repeat time + // It is not used for WALLCLOCK or INDEFINITE times + SMILTimeValue mOffset; + + // The base element that this specification refers to. + // For SYNCBASE types, this is the timebase + // For EVENT and REPEAT types, this is the eventbase + RefPtr<nsAtom> mDependentElemID; + + // The event to respond to. + // Only used for EVENT types. + RefPtr<nsAtom> mEventSymbol; + + // Indicates if this specification refers to the begin or end of the dependent + // element. + // Only used for SYNCBASE types. + bool mSyncBegin; + + // The repeat iteration to respond to. + // Only used for mType=REPEAT. + uint32_t mRepeatIteration; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMEVALUESPECPARAMS_H_ diff --git a/dom/smil/SMILTimedElement.cpp b/dom/smil/SMILTimedElement.cpp new file mode 100644 index 0000000000..fc8111453f --- /dev/null +++ b/dom/smil/SMILTimedElement.cpp @@ -0,0 +1,2180 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILTimedElement.h" + +#include "mozilla/AutoRestore.h" +#include "mozilla/ContentEvents.h" +#include "mozilla/DebugOnly.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/SMILAnimationFunction.h" +#include "mozilla/SMILInstanceTime.h" +#include "mozilla/SMILParserUtils.h" +#include "mozilla/SMILTimeContainer.h" +#include "mozilla/SMILTimeValue.h" +#include "mozilla/SMILTimeValueSpec.h" +#include "mozilla/TaskCategory.h" +#include "mozilla/dom/DocumentInlines.h" +#include "mozilla/dom/SVGAnimationElement.h" +#include "nsAttrValueInlines.h" +#include "nsGkAtoms.h" +#include "nsReadableUtils.h" +#include "nsMathUtils.h" +#include "nsThreadUtils.h" +#include "prdtoa.h" +#include "plstr.h" +#include "prtime.h" +#include "nsString.h" +#include "nsCharSeparatedTokenizer.h" +#include <algorithm> + +using namespace mozilla::dom; + +namespace mozilla { + +//---------------------------------------------------------------------- +// Helper class: InstanceTimeComparator + +// Upon inserting an instance time into one of our instance time lists we assign +// it a serial number. This allows us to sort the instance times in such a way +// that where we have several equal instance times, the ones added later will +// sort later. This means that when we call UpdateCurrentInterval during the +// waiting state we won't unnecessarily change the begin instance. +// +// The serial number also means that every instance time has an unambiguous +// position in the array so we can use RemoveElementSorted and the like. +bool SMILTimedElement::InstanceTimeComparator::Equals( + const SMILInstanceTime* aElem1, const SMILInstanceTime* aElem2) const { + MOZ_ASSERT(aElem1 && aElem2, "Trying to compare null instance time pointers"); + MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(), + "Instance times have not been assigned serial numbers"); + MOZ_ASSERT(aElem1 == aElem2 || aElem1->Serial() != aElem2->Serial(), + "Serial numbers are not unique"); + + return aElem1->Serial() == aElem2->Serial(); +} + +bool SMILTimedElement::InstanceTimeComparator::LessThan( + const SMILInstanceTime* aElem1, const SMILInstanceTime* aElem2) const { + MOZ_ASSERT(aElem1 && aElem2, "Trying to compare null instance time pointers"); + MOZ_ASSERT(aElem1->Serial() && aElem2->Serial(), + "Instance times have not been assigned serial numbers"); + + int8_t cmp = aElem1->Time().CompareTo(aElem2->Time()); + return cmp == 0 ? aElem1->Serial() < aElem2->Serial() : cmp < 0; +} + +//---------------------------------------------------------------------- +// Helper class: AsyncTimeEventRunner + +namespace { +class AsyncTimeEventRunner : public Runnable { + protected: + const RefPtr<nsIContent> mTarget; + EventMessage mMsg; + int32_t mDetail; + + public: + AsyncTimeEventRunner(nsIContent* aTarget, EventMessage aMsg, int32_t aDetail) + : mozilla::Runnable("AsyncTimeEventRunner"), + mTarget(aTarget), + mMsg(aMsg), + mDetail(aDetail) {} + + // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230, bug 1535398) + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + InternalSMILTimeEvent event(true, mMsg); + event.mDetail = mDetail; + + RefPtr<nsPresContext> context = nullptr; + Document* doc = mTarget->GetComposedDoc(); + if (doc) { + context = doc->GetPresContext(); + } + + return EventDispatcher::Dispatch(mTarget, context, &event); + } +}; +} // namespace + +//---------------------------------------------------------------------- +// Helper class: AutoIntervalUpdateBatcher + +// Stack-based helper class to set the mDeferIntervalUpdates flag on an +// SMILTimedElement and perform the UpdateCurrentInterval when the object is +// destroyed. +// +// If several of these objects are allocated on the stack, the update will not +// be performed until the last object for a given SMILTimedElement is +// destroyed. +class MOZ_STACK_CLASS SMILTimedElement::AutoIntervalUpdateBatcher { + public: + explicit AutoIntervalUpdateBatcher(SMILTimedElement& aTimedElement) + : mTimedElement(aTimedElement), + mDidSetFlag(!aTimedElement.mDeferIntervalUpdates) { + mTimedElement.mDeferIntervalUpdates = true; + } + + ~AutoIntervalUpdateBatcher() { + if (!mDidSetFlag) return; + + mTimedElement.mDeferIntervalUpdates = false; + + if (mTimedElement.mDoDeferredUpdate) { + mTimedElement.mDoDeferredUpdate = false; + mTimedElement.UpdateCurrentInterval(); + } + } + + private: + SMILTimedElement& mTimedElement; + bool mDidSetFlag; +}; + +//---------------------------------------------------------------------- +// Helper class: AutoIntervalUpdater + +// Stack-based helper class to call UpdateCurrentInterval when it is destroyed +// which helps avoid bugs where we forget to call UpdateCurrentInterval in the +// case of early returns (e.g. due to parse errors). +// +// This can be safely used in conjunction with AutoIntervalUpdateBatcher; any +// calls to UpdateCurrentInterval made by this class will simply be deferred if +// there is an AutoIntervalUpdateBatcher on the stack. +class MOZ_STACK_CLASS SMILTimedElement::AutoIntervalUpdater { + public: + explicit AutoIntervalUpdater(SMILTimedElement& aTimedElement) + : mTimedElement(aTimedElement) {} + + ~AutoIntervalUpdater() { mTimedElement.UpdateCurrentInterval(); } + + private: + SMILTimedElement& mTimedElement; +}; + +//---------------------------------------------------------------------- +// Templated helper functions + +// Selectively remove elements from an array of type +// nsTArray<RefPtr<SMILInstanceTime> > with O(n) performance. +template <class TestFunctor> +void SMILTimedElement::RemoveInstanceTimes(InstanceTimeList& aArray, + TestFunctor& aTest) { + InstanceTimeList newArray; + for (uint32_t i = 0; i < aArray.Length(); ++i) { + SMILInstanceTime* item = aArray[i].get(); + if (aTest(item, i)) { + // As per bugs 665334 and 669225 we should be careful not to remove the + // instance time that corresponds to the previous interval's end time. + // + // Most functors supplied here fulfil this condition by checking if the + // instance time is marked as "ShouldPreserve" and if so, not deleting it. + // + // However, when filtering instance times, we sometimes need to drop even + // instance times marked as "ShouldPreserve". In that case we take special + // care not to delete the end instance time of the previous interval. + MOZ_ASSERT(!GetPreviousInterval() || item != GetPreviousInterval()->End(), + "Removing end instance time of previous interval"); + item->Unlink(); + } else { + newArray.AppendElement(item); + } + } + aArray = std::move(newArray); +} + +//---------------------------------------------------------------------- +// Static members + +const nsAttrValue::EnumTable SMILTimedElement::sFillModeTable[] = { + {"remove", FILL_REMOVE}, {"freeze", FILL_FREEZE}, {nullptr, 0}}; + +const nsAttrValue::EnumTable SMILTimedElement::sRestartModeTable[] = { + {"always", RESTART_ALWAYS}, + {"whenNotActive", RESTART_WHENNOTACTIVE}, + {"never", RESTART_NEVER}, + {nullptr, 0}}; + +const SMILMilestone SMILTimedElement::sMaxMilestone( + std::numeric_limits<SMILTime>::max(), false); + +// The thresholds at which point we start filtering intervals and instance times +// indiscriminately. +// See FilterIntervals and FilterInstanceTimes. +const uint8_t SMILTimedElement::sMaxNumIntervals = 20; +const uint8_t SMILTimedElement::sMaxNumInstanceTimes = 100; + +// Detect if we arrive in some sort of undetected recursive syncbase dependency +// relationship +const uint8_t SMILTimedElement::sMaxUpdateIntervalRecursionDepth = 20; + +//---------------------------------------------------------------------- +// Ctor, dtor + +SMILTimedElement::SMILTimedElement() + : mAnimationElement(nullptr), + mFillMode(FILL_REMOVE), + mRestartMode(RESTART_ALWAYS), + mInstanceSerialIndex(0), + mClient(nullptr), + mCurrentInterval(nullptr), + mCurrentRepeatIteration(0), + mPrevRegisteredMilestone(sMaxMilestone), + mElementState(STATE_STARTUP), + mSeekState(SEEK_NOT_SEEKING), + mDeferIntervalUpdates(false), + mDoDeferredUpdate(false), + mIsDisabled(false), + mDeleteCount(0), + mUpdateIntervalRecursionDepth(0) { + mSimpleDur.SetIndefinite(); + mMin.SetMillis(0L); + mMax.SetIndefinite(); +} + +SMILTimedElement::~SMILTimedElement() { + // Unlink all instance times from dependent intervals + for (uint32_t i = 0; i < mBeginInstances.Length(); ++i) { + mBeginInstances[i]->Unlink(); + } + mBeginInstances.Clear(); + for (uint32_t i = 0; i < mEndInstances.Length(); ++i) { + mEndInstances[i]->Unlink(); + } + mEndInstances.Clear(); + + // Notify anyone listening to our intervals that they're gone + // (We shouldn't get any callbacks from this because all our instance times + // are now disassociated with any intervals) + ClearIntervals(); + + // The following assertions are important in their own right (for checking + // correct behavior) but also because AutoIntervalUpdateBatcher holds pointers + // to class so if they fail there's the possibility we might have dangling + // pointers. + MOZ_ASSERT(!mDeferIntervalUpdates, + "Interval updates should no longer be blocked when an " + "SMILTimedElement disappears"); + MOZ_ASSERT(!mDoDeferredUpdate, + "There should no longer be any pending updates when an " + "SMILTimedElement disappears"); +} + +void SMILTimedElement::SetAnimationElement(SVGAnimationElement* aElement) { + MOZ_ASSERT(aElement, "NULL owner element"); + MOZ_ASSERT(!mAnimationElement, "Re-setting owner"); + mAnimationElement = aElement; +} + +SMILTimeContainer* SMILTimedElement::GetTimeContainer() { + return mAnimationElement ? mAnimationElement->GetTimeContainer() : nullptr; +} + +dom::Element* SMILTimedElement::GetTargetElement() { + return mAnimationElement ? mAnimationElement->GetTargetElementContent() + : nullptr; +} + +//---------------------------------------------------------------------- +// ElementTimeControl methods +// +// The definition of the ElementTimeControl interface differs between SMIL +// Animation and SVG 1.1. In SMIL Animation all methods have a void return +// type and the new instance time is simply added to the list and restart +// semantics are applied as with any other instance time. In the SVG definition +// the methods return a bool depending on the restart mode. +// +// This inconsistency has now been addressed by an erratum in SVG 1.1: +// +// http://www.w3.org/2003/01/REC-SVG11-20030114-errata#elementtimecontrol-interface +// +// which favours the definition in SMIL, i.e. instance times are just added +// without first checking the restart mode. + +nsresult SMILTimedElement::BeginElementAt(double aOffsetSeconds) { + SMILTimeContainer* container = GetTimeContainer(); + if (!container) return NS_ERROR_FAILURE; + + SMILTime currentTime = container->GetCurrentTimeAsSMILTime(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, true); +} + +nsresult SMILTimedElement::EndElementAt(double aOffsetSeconds) { + SMILTimeContainer* container = GetTimeContainer(); + if (!container) return NS_ERROR_FAILURE; + + SMILTime currentTime = container->GetCurrentTimeAsSMILTime(); + return AddInstanceTimeFromCurrentTime(currentTime, aOffsetSeconds, false); +} + +//---------------------------------------------------------------------- +// SVGAnimationElement methods + +SMILTimeValue SMILTimedElement::GetStartTime() const { + return mElementState == STATE_WAITING || mElementState == STATE_ACTIVE + ? mCurrentInterval->Begin()->Time() + : SMILTimeValue(); +} + +//---------------------------------------------------------------------- +// Hyperlinking support + +SMILTimeValue SMILTimedElement::GetHyperlinkTime() const { + SMILTimeValue hyperlinkTime; // Default ctor creates unresolved time + + if (mElementState == STATE_ACTIVE) { + hyperlinkTime = mCurrentInterval->Begin()->Time(); + } else if (!mBeginInstances.IsEmpty()) { + hyperlinkTime = mBeginInstances[0]->Time(); + } + + return hyperlinkTime; +} + +//---------------------------------------------------------------------- +// SMILTimedElement + +void SMILTimedElement::AddInstanceTime(SMILInstanceTime* aInstanceTime, + bool aIsBegin) { + MOZ_ASSERT(aInstanceTime, "Attempting to add null instance time"); + + // Event-sensitivity: If an element is not active (but the parent time + // container is), then events are only handled for begin specifications. + if (mElementState != STATE_ACTIVE && !aIsBegin && + aInstanceTime->IsDynamic()) { + // No need to call Unlink here--dynamic instance times shouldn't be linked + // to anything that's going to miss them + MOZ_ASSERT(!aInstanceTime->GetBaseInterval(), + "Dynamic instance time has a base interval--we probably need " + "to unlink it if we're not going to use it"); + return; + } + + aInstanceTime->SetSerial(++mInstanceSerialIndex); + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + RefPtr<SMILInstanceTime>* inserted = + instanceList.InsertElementSorted(aInstanceTime, InstanceTimeComparator()); + if (!inserted) { + NS_WARNING("Insufficient memory to insert instance time"); + return; + } + + UpdateCurrentInterval(); +} + +void SMILTimedElement::UpdateInstanceTime(SMILInstanceTime* aInstanceTime, + SMILTimeValue& aUpdatedTime, + bool aIsBegin) { + MOZ_ASSERT(aInstanceTime, "Attempting to update null instance time"); + + // The reason we update the time here and not in the SMILTimeValueSpec is + // that it means we *could* re-sort more efficiently by doing a sorted remove + // and insert but currently this doesn't seem to be necessary given how + // infrequently we get these change notices. + aInstanceTime->DependentUpdate(aUpdatedTime); + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + instanceList.Sort(InstanceTimeComparator()); + + // Generally speaking, UpdateCurrentInterval makes changes to the current + // interval and sends changes notices itself. However, in this case because + // instance times are shared between the instance time list and the intervals + // we are effectively changing the current interval outside + // UpdateCurrentInterval so we need to explicitly signal that we've made + // a change. + // + // This wouldn't be necessary if we cloned instance times on adding them to + // the current interval but this introduces other complications (particularly + // detecting which instance time is being used to define the begin of the + // current interval when doing a Reset). + bool changedCurrentInterval = + mCurrentInterval && (mCurrentInterval->Begin() == aInstanceTime || + mCurrentInterval->End() == aInstanceTime); + + UpdateCurrentInterval(changedCurrentInterval); +} + +void SMILTimedElement::RemoveInstanceTime(SMILInstanceTime* aInstanceTime, + bool aIsBegin) { + MOZ_ASSERT(aInstanceTime, "Attempting to remove null instance time"); + + // If the instance time should be kept (because it is or was the fixed end + // point of an interval) then just disassociate it from the creator. + if (aInstanceTime->ShouldPreserve()) { + aInstanceTime->Unlink(); + return; + } + + InstanceTimeList& instanceList = aIsBegin ? mBeginInstances : mEndInstances; + mozilla::DebugOnly<bool> found = + instanceList.RemoveElementSorted(aInstanceTime, InstanceTimeComparator()); + MOZ_ASSERT(found, "Couldn't find instance time to delete"); + + UpdateCurrentInterval(); +} + +namespace { +class MOZ_STACK_CLASS RemoveByCreator { + public: + explicit RemoveByCreator(const SMILTimeValueSpec* aCreator) + : mCreator(aCreator) {} + + bool operator()(SMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) { + if (aInstanceTime->GetCreator() != mCreator) return false; + + // If the instance time should be kept (because it is or was the fixed end + // point of an interval) then just disassociate it from the creator. + if (aInstanceTime->ShouldPreserve()) { + aInstanceTime->Unlink(); + return false; + } + + return true; + } + + private: + const SMILTimeValueSpec* mCreator; +}; +} // namespace + +void SMILTimedElement::RemoveInstanceTimesForCreator( + const SMILTimeValueSpec* aCreator, bool aIsBegin) { + MOZ_ASSERT(aCreator, "Creator not set"); + + InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances; + RemoveByCreator removeByCreator(aCreator); + RemoveInstanceTimes(instances, removeByCreator); + + UpdateCurrentInterval(); +} + +void SMILTimedElement::SetTimeClient(SMILAnimationFunction* aClient) { + // + // No need to check for nullptr. A nullptr parameter simply means to remove + // the previous client which we do by setting to nullptr anyway. + // + + mClient = aClient; +} + +void SMILTimedElement::SampleAt(SMILTime aContainerTime) { + if (mIsDisabled) return; + + // Milestones are cleared before a sample + mPrevRegisteredMilestone = sMaxMilestone; + + DoSampleAt(aContainerTime, false); +} + +void SMILTimedElement::SampleEndAt(SMILTime aContainerTime) { + if (mIsDisabled) return; + + // Milestones are cleared before a sample + mPrevRegisteredMilestone = sMaxMilestone; + + // If the current interval changes, we don't bother trying to remove any old + // milestones we'd registered. So it's possible to get a call here to end an + // interval at a time that no longer reflects the end of the current interval. + // + // For now we just check that we're actually in an interval but note that the + // initial sample we use to initialise the model is an end sample. This is + // because we want to resolve all the instance times before committing to an + // initial interval. Therefore an end sample from the startup state is also + // acceptable. + if (mElementState == STATE_ACTIVE || mElementState == STATE_STARTUP) { + DoSampleAt(aContainerTime, true); // End sample + } else { + // Even if this was an unnecessary milestone sample we want to be sure that + // our next real milestone is registered. + RegisterMilestone(); + } +} + +void SMILTimedElement::DoSampleAt(SMILTime aContainerTime, bool aEndOnly) { + MOZ_ASSERT(mAnimationElement, + "Got sample before being registered with an animation element"); + MOZ_ASSERT(GetTimeContainer(), + "Got sample without being registered with a time container"); + + // This could probably happen if we later implement externalResourcesRequired + // (bug 277955) and whilst waiting for those resources (and the animation to + // start) we transfer a node from another document fragment that has already + // started. In such a case we might receive milestone samples registered with + // the already active container. + if (GetTimeContainer()->IsPausedByType(SMILTimeContainer::PAUSE_BEGIN)) + return; + + // We use an end-sample to start animation since an end-sample lets us + // tentatively create an interval without committing to it (by transitioning + // to the ACTIVE state) and this is necessary because we might have + // dependencies on other animations that are yet to start. After these + // other animations start, it may be necessary to revise our initial interval. + // + // However, sometimes instead of an end-sample we can get a regular sample + // during STARTUP state. This can happen, for example, if we register + // a milestone before time t=0 and are then re-bound to the tree (which sends + // us back to the STARTUP state). In such a case we should just ignore the + // sample and wait for our real initial sample which will be an end-sample. + if (mElementState == STATE_STARTUP && !aEndOnly) return; + + bool finishedSeek = false; + if (GetTimeContainer()->IsSeeking() && mSeekState == SEEK_NOT_SEEKING) { + mSeekState = mElementState == STATE_ACTIVE ? SEEK_FORWARD_FROM_ACTIVE + : SEEK_FORWARD_FROM_INACTIVE; + } else if (mSeekState != SEEK_NOT_SEEKING && + !GetTimeContainer()->IsSeeking()) { + finishedSeek = true; + } + + bool stateChanged; + SMILTimeValue sampleTime(aContainerTime); + + do { +#ifdef DEBUG + // Check invariant + if (mElementState == STATE_STARTUP || mElementState == STATE_POSTACTIVE) { + MOZ_ASSERT(!mCurrentInterval, + "Shouldn't have current interval in startup or postactive " + "states"); + } else { + MOZ_ASSERT(mCurrentInterval, + "Should have current interval in waiting and active states"); + } +#endif + + stateChanged = false; + + switch (mElementState) { + case STATE_STARTUP: { + SMILInterval firstInterval; + mElementState = + GetNextInterval(nullptr, nullptr, nullptr, firstInterval) + ? STATE_WAITING + : STATE_POSTACTIVE; + stateChanged = true; + if (mElementState == STATE_WAITING) { + mCurrentInterval = MakeUnique<SMILInterval>(firstInterval); + NotifyNewInterval(); + } + } break; + + case STATE_WAITING: { + if (mCurrentInterval->Begin()->Time() <= sampleTime) { + mElementState = STATE_ACTIVE; + mCurrentInterval->FixBegin(); + if (mClient) { + mClient->Activate(mCurrentInterval->Begin()->Time().GetMillis()); + } + if (mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILBeginEvent, 0); + } + if (HasPlayed()) { + Reset(); // Apply restart behaviour + // The call to Reset() may mean that the end point of our current + // interval should be changed and so we should update the interval + // now. However, calling UpdateCurrentInterval could result in the + // interval getting deleted (perhaps through some web of syncbase + // dependencies) therefore we make updating the interval the last + // thing we do. There is no guarantee that mCurrentInterval is set + // after this. + UpdateCurrentInterval(); + } + stateChanged = true; + } + } break; + + case STATE_ACTIVE: { + // Ending early will change the interval but we don't notify dependents + // of the change until we have closed off the current interval (since we + // don't want dependencies to un-end our early end). + bool didApplyEarlyEnd = ApplyEarlyEnd(sampleTime); + + if (mCurrentInterval->End()->Time() <= sampleTime) { + SMILInterval newInterval; + mElementState = GetNextInterval(mCurrentInterval.get(), nullptr, + nullptr, newInterval) + ? STATE_WAITING + : STATE_POSTACTIVE; + if (mClient) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + } + mCurrentInterval->FixEnd(); + if (mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILEndEvent, 0); + } + mCurrentRepeatIteration = 0; + mOldIntervals.AppendElement(std::move(mCurrentInterval)); + SampleFillValue(); + if (mElementState == STATE_WAITING) { + mCurrentInterval = MakeUnique<SMILInterval>(newInterval); + } + // We are now in a consistent state to dispatch notifications + if (didApplyEarlyEnd) { + NotifyChangedInterval( + mOldIntervals[mOldIntervals.Length() - 1].get(), false, true); + } + if (mElementState == STATE_WAITING) { + NotifyNewInterval(); + } + FilterHistory(); + stateChanged = true; + } else if (mCurrentInterval->Begin()->Time() <= sampleTime) { + MOZ_ASSERT(!didApplyEarlyEnd, "We got an early end, but didn't end"); + SMILTime beginTime = mCurrentInterval->Begin()->Time().GetMillis(); + SMILTime activeTime = aContainerTime - beginTime; + + // The 'min' attribute can cause the active interval to be longer than + // the 'repeating interval'. + // In that extended period we apply the fill mode. + if (GetRepeatDuration() <= SMILTimeValue(activeTime)) { + if (mClient && mClient->IsActive()) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + } + SampleFillValue(); + } else { + SampleSimpleTime(activeTime); + + // We register our repeat times as milestones (except when we're + // seeking) so we should get a sample at exactly the time we repeat. + // (And even when we are seeking we want to update + // mCurrentRepeatIteration so we do that first before testing the + // seek state.) + uint32_t prevRepeatIteration = mCurrentRepeatIteration; + if (ActiveTimeToSimpleTime(activeTime, mCurrentRepeatIteration) == + 0 && + mCurrentRepeatIteration != prevRepeatIteration && + mCurrentRepeatIteration && mSeekState == SEEK_NOT_SEEKING) { + FireTimeEventAsync(eSMILRepeatEvent, + static_cast<int32_t>(mCurrentRepeatIteration)); + } + } + } + // Otherwise |sampleTime| is *before* the current interval. That + // normally doesn't happen but can happen if we get a stray milestone + // sample (e.g. if we registered a milestone with a time container that + // later got re-attached as a child of a more advanced time container). + // In that case we should just ignore the sample. + } break; + + case STATE_POSTACTIVE: + break; + } + + // Generally we continue driving the state machine so long as we have + // changed state. However, for end samples we only drive the state machine + // as far as the waiting or postactive state because we don't want to commit + // to any new interval (by transitioning to the active state) until all the + // end samples have finished and we then have complete information about the + // available instance times upon which to base our next interval. + } while (stateChanged && (!aEndOnly || (mElementState != STATE_WAITING && + mElementState != STATE_POSTACTIVE))); + + if (finishedSeek) { + DoPostSeek(); + } + RegisterMilestone(); +} + +void SMILTimedElement::HandleContainerTimeChange() { + // In future we could possibly introduce a separate change notice for time + // container changes and only notify those dependents who live in other time + // containers. For now we don't bother because when we re-resolve the time in + // the SMILTimeValueSpec we'll check if anything has changed and if not, we + // won't go any further. + if (mElementState == STATE_WAITING || mElementState == STATE_ACTIVE) { + NotifyChangedInterval(mCurrentInterval.get(), false, false); + } +} + +namespace { +bool RemoveNonDynamic(SMILInstanceTime* aInstanceTime) { + // Generally dynamically-generated instance times (DOM calls, event-based + // times) are not associated with their creator SMILTimeValueSpec since + // they may outlive them. + MOZ_ASSERT(!aInstanceTime->IsDynamic() || !aInstanceTime->GetCreator(), + "Dynamic instance time should be unlinked from its creator"); + return !aInstanceTime->IsDynamic() && !aInstanceTime->ShouldPreserve(); +} +} // namespace + +void SMILTimedElement::Rewind() { + MOZ_ASSERT(mAnimationElement, + "Got rewind request before being attached to an animation " + "element"); + + // It's possible to get a rewind request whilst we're already in the middle of + // a backwards seek. This can happen when we're performing tree surgery and + // seeking containers at the same time because we can end up requesting + // a local rewind on an element after binding it to a new container and then + // performing a rewind on that container as a whole without sampling in + // between. + // + // However, it should currently be impossible to get a rewind in the middle of + // a forwards seek since forwards seeks are detected and processed within the + // same (re)sample. + if (mSeekState == SEEK_NOT_SEEKING) { + mSeekState = mElementState == STATE_ACTIVE ? SEEK_BACKWARD_FROM_ACTIVE + : SEEK_BACKWARD_FROM_INACTIVE; + } + MOZ_ASSERT(mSeekState == SEEK_BACKWARD_FROM_INACTIVE || + mSeekState == SEEK_BACKWARD_FROM_ACTIVE, + "Rewind in the middle of a forwards seek?"); + + ClearTimingState(RemoveNonDynamic); + RebuildTimingState(RemoveNonDynamic); + + MOZ_ASSERT(!mCurrentInterval, "Current interval is set at end of rewind"); +} + +namespace { +bool RemoveAll(SMILInstanceTime* aInstanceTime) { return true; } +} // namespace + +bool SMILTimedElement::SetIsDisabled(bool aIsDisabled) { + if (mIsDisabled == aIsDisabled) return false; + + if (aIsDisabled) { + mIsDisabled = true; + ClearTimingState(RemoveAll); + } else { + RebuildTimingState(RemoveAll); + mIsDisabled = false; + } + return true; +} + +namespace { +bool RemoveNonDOM(SMILInstanceTime* aInstanceTime) { + return !aInstanceTime->FromDOM() && !aInstanceTime->ShouldPreserve(); +} +} // namespace + +bool SMILTimedElement::SetAttr(nsAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, Element& aContextElement, + nsresult* aParseResult) { + bool foundMatch = true; + nsresult parseResult = NS_OK; + + if (aAttribute == nsGkAtoms::begin) { + parseResult = SetBeginSpec(aValue, aContextElement, RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::dur) { + parseResult = SetSimpleDuration(aValue); + } else if (aAttribute == nsGkAtoms::end) { + parseResult = SetEndSpec(aValue, aContextElement, RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::fill) { + parseResult = SetFillMode(aValue); + } else if (aAttribute == nsGkAtoms::max) { + parseResult = SetMax(aValue); + } else if (aAttribute == nsGkAtoms::min) { + parseResult = SetMin(aValue); + } else if (aAttribute == nsGkAtoms::repeatCount) { + parseResult = SetRepeatCount(aValue); + } else if (aAttribute == nsGkAtoms::repeatDur) { + parseResult = SetRepeatDur(aValue); + } else if (aAttribute == nsGkAtoms::restart) { + parseResult = SetRestart(aValue); + } else { + foundMatch = false; + } + + if (foundMatch) { + aResult.SetTo(aValue); + if (aParseResult) { + *aParseResult = parseResult; + } + } + + return foundMatch; +} + +bool SMILTimedElement::UnsetAttr(nsAtom* aAttribute) { + bool foundMatch = true; + + if (aAttribute == nsGkAtoms::begin) { + UnsetBeginSpec(RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::dur) { + UnsetSimpleDuration(); + } else if (aAttribute == nsGkAtoms::end) { + UnsetEndSpec(RemoveNonDOM); + } else if (aAttribute == nsGkAtoms::fill) { + UnsetFillMode(); + } else if (aAttribute == nsGkAtoms::max) { + UnsetMax(); + } else if (aAttribute == nsGkAtoms::min) { + UnsetMin(); + } else if (aAttribute == nsGkAtoms::repeatCount) { + UnsetRepeatCount(); + } else if (aAttribute == nsGkAtoms::repeatDur) { + UnsetRepeatDur(); + } else if (aAttribute == nsGkAtoms::restart) { + UnsetRestart(); + } else { + foundMatch = false; + } + + return foundMatch; +} + +//---------------------------------------------------------------------- +// Setters and unsetters + +nsresult SMILTimedElement::SetBeginSpec(const nsAString& aBeginSpec, + Element& aContextElement, + RemovalTestFunction aRemove) { + return SetBeginOrEndSpec(aBeginSpec, aContextElement, true /*isBegin*/, + aRemove); +} + +void SMILTimedElement::UnsetBeginSpec(RemovalTestFunction aRemove) { + ClearSpecs(mBeginSpecs, mBeginInstances, aRemove); + UpdateCurrentInterval(); +} + +nsresult SMILTimedElement::SetEndSpec(const nsAString& aEndSpec, + Element& aContextElement, + RemovalTestFunction aRemove) { + return SetBeginOrEndSpec(aEndSpec, aContextElement, false /*!isBegin*/, + aRemove); +} + +void SMILTimedElement::UnsetEndSpec(RemovalTestFunction aRemove) { + ClearSpecs(mEndSpecs, mEndInstances, aRemove); + UpdateCurrentInterval(); +} + +nsresult SMILTimedElement::SetSimpleDuration(const nsAString& aDurSpec) { + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + SMILTimeValue duration; + const nsAString& dur = SMILParserUtils::TrimWhitespace(aDurSpec); + + // SVG-specific: "For SVG's animation elements, if "media" is specified, the + // attribute will be ignored." (SVG 1.1, section 19.2.6) + if (dur.EqualsLiteral("media") || dur.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!SMILParserUtils::ParseClockValue(dur, &duration) || + duration.GetMillis() == 0L) { + mSimpleDur.SetIndefinite(); + return NS_ERROR_FAILURE; + } + } + // mSimpleDur should never be unresolved. ParseClockValue will either set + // duration to resolved or will return false. + MOZ_ASSERT(duration.IsResolved(), "Setting unresolved simple duration"); + + mSimpleDur = duration; + + return NS_OK; +} + +void SMILTimedElement::UnsetSimpleDuration() { + mSimpleDur.SetIndefinite(); + UpdateCurrentInterval(); +} + +nsresult SMILTimedElement::SetMin(const nsAString& aMinSpec) { + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + SMILTimeValue duration; + const nsAString& min = SMILParserUtils::TrimWhitespace(aMinSpec); + + if (min.EqualsLiteral("media")) { + duration.SetMillis(0L); + } else { + if (!SMILParserUtils::ParseClockValue(min, &duration)) { + mMin.SetMillis(0L); + return NS_ERROR_FAILURE; + } + } + + MOZ_ASSERT(duration.GetMillis() >= 0L, "Invalid duration"); + + mMin = duration; + + return NS_OK; +} + +void SMILTimedElement::UnsetMin() { + mMin.SetMillis(0L); + UpdateCurrentInterval(); +} + +nsresult SMILTimedElement::SetMax(const nsAString& aMaxSpec) { + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + SMILTimeValue duration; + const nsAString& max = SMILParserUtils::TrimWhitespace(aMaxSpec); + + if (max.EqualsLiteral("media") || max.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!SMILParserUtils::ParseClockValue(max, &duration) || + duration.GetMillis() == 0L) { + mMax.SetIndefinite(); + return NS_ERROR_FAILURE; + } + MOZ_ASSERT(duration.GetMillis() > 0L, "Invalid duration"); + } + + mMax = duration; + + return NS_OK; +} + +void SMILTimedElement::UnsetMax() { + mMax.SetIndefinite(); + UpdateCurrentInterval(); +} + +nsresult SMILTimedElement::SetRestart(const nsAString& aRestartSpec) { + nsAttrValue temp; + bool parseResult = temp.ParseEnumValue(aRestartSpec, sRestartModeTable, true); + mRestartMode = + parseResult ? SMILRestartMode(temp.GetEnumValue()) : RESTART_ALWAYS; + UpdateCurrentInterval(); + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILTimedElement::UnsetRestart() { + mRestartMode = RESTART_ALWAYS; + UpdateCurrentInterval(); +} + +nsresult SMILTimedElement::SetRepeatCount(const nsAString& aRepeatCountSpec) { + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + SMILRepeatCount newRepeatCount; + + if (SMILParserUtils::ParseRepeatCount(aRepeatCountSpec, newRepeatCount)) { + mRepeatCount = newRepeatCount; + return NS_OK; + } + mRepeatCount.Unset(); + return NS_ERROR_FAILURE; +} + +void SMILTimedElement::UnsetRepeatCount() { + mRepeatCount.Unset(); + UpdateCurrentInterval(); +} + +nsresult SMILTimedElement::SetRepeatDur(const nsAString& aRepeatDurSpec) { + // Update the current interval before returning + AutoIntervalUpdater updater(*this); + + SMILTimeValue duration; + + const nsAString& repeatDur = SMILParserUtils::TrimWhitespace(aRepeatDurSpec); + + if (repeatDur.EqualsLiteral("indefinite")) { + duration.SetIndefinite(); + } else { + if (!SMILParserUtils::ParseClockValue(repeatDur, &duration)) { + mRepeatDur.SetUnresolved(); + return NS_ERROR_FAILURE; + } + } + + mRepeatDur = duration; + + return NS_OK; +} + +void SMILTimedElement::UnsetRepeatDur() { + mRepeatDur.SetUnresolved(); + UpdateCurrentInterval(); +} + +nsresult SMILTimedElement::SetFillMode(const nsAString& aFillModeSpec) { + uint16_t previousFillMode = mFillMode; + + nsAttrValue temp; + bool parseResult = temp.ParseEnumValue(aFillModeSpec, sFillModeTable, true); + mFillMode = parseResult ? SMILFillMode(temp.GetEnumValue()) : FILL_REMOVE; + + // Update fill mode of client + if (mFillMode != previousFillMode && HasClientInFillRange()) { + mClient->Inactivate(mFillMode == FILL_FREEZE); + SampleFillValue(); + } + + return parseResult ? NS_OK : NS_ERROR_FAILURE; +} + +void SMILTimedElement::UnsetFillMode() { + uint16_t previousFillMode = mFillMode; + mFillMode = FILL_REMOVE; + if (previousFillMode == FILL_FREEZE && HasClientInFillRange()) { + mClient->Inactivate(false); + } +} + +void SMILTimedElement::AddDependent(SMILTimeValueSpec& aDependent) { + // There's probably no harm in attempting to register a dependent + // SMILTimeValueSpec twice, but we're not expecting it to happen. + MOZ_ASSERT(!mTimeDependents.GetEntry(&aDependent), + "SMILTimeValueSpec is already registered as a dependency"); + mTimeDependents.PutEntry(&aDependent); + + // Add current interval. We could add historical intervals too but that would + // cause unpredictable results since some intervals may have been filtered. + // SMIL doesn't say what to do here so for simplicity and consistency we + // simply add the current interval if there is one. + // + // It's not necessary to call SyncPauseTime since we're dealing with + // historical instance times not newly added ones. + if (mCurrentInterval) { + aDependent.HandleNewInterval(*mCurrentInterval, GetTimeContainer()); + } +} + +void SMILTimedElement::RemoveDependent(SMILTimeValueSpec& aDependent) { + mTimeDependents.RemoveEntry(&aDependent); +} + +bool SMILTimedElement::IsTimeDependent(const SMILTimedElement& aOther) const { + const SMILInstanceTime* thisBegin = GetEffectiveBeginInstance(); + const SMILInstanceTime* otherBegin = aOther.GetEffectiveBeginInstance(); + + if (!thisBegin || !otherBegin) return false; + + return thisBegin->IsDependentOn(*otherBegin); +} + +void SMILTimedElement::BindToTree(Element& aContextElement) { + // Reset previously registered milestone since we may be registering with + // a different time container now. + mPrevRegisteredMilestone = sMaxMilestone; + + // If we were already active then clear all our timing information and start + // afresh + if (mElementState != STATE_STARTUP) { + mSeekState = SEEK_NOT_SEEKING; + Rewind(); + } + + // Scope updateBatcher to last only for the ResolveReferences calls: + { + AutoIntervalUpdateBatcher updateBatcher(*this); + + // Resolve references to other parts of the tree + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + mBeginSpecs[i]->ResolveReferences(aContextElement); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + mEndSpecs[j]->ResolveReferences(aContextElement); + } + } + + RegisterMilestone(); +} + +void SMILTimedElement::HandleTargetElementChange(Element* aNewTarget) { + AutoIntervalUpdateBatcher updateBatcher(*this); + + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + mBeginSpecs[i]->HandleTargetElementChange(aNewTarget); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + mEndSpecs[j]->HandleTargetElementChange(aNewTarget); + } +} + +void SMILTimedElement::Traverse(nsCycleCollectionTraversalCallback* aCallback) { + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + SMILTimeValueSpec* beginSpec = mBeginSpecs[i].get(); + MOZ_ASSERT(beginSpec, "null SMILTimeValueSpec in list of begin specs"); + beginSpec->Traverse(aCallback); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + SMILTimeValueSpec* endSpec = mEndSpecs[j].get(); + MOZ_ASSERT(endSpec, "null SMILTimeValueSpec in list of end specs"); + endSpec->Traverse(aCallback); + } +} + +void SMILTimedElement::Unlink() { + AutoIntervalUpdateBatcher updateBatcher(*this); + + // Remove dependencies on other elements + uint32_t count = mBeginSpecs.Length(); + for (uint32_t i = 0; i < count; ++i) { + SMILTimeValueSpec* beginSpec = mBeginSpecs[i].get(); + MOZ_ASSERT(beginSpec, "null SMILTimeValueSpec in list of begin specs"); + beginSpec->Unlink(); + } + + count = mEndSpecs.Length(); + for (uint32_t j = 0; j < count; ++j) { + SMILTimeValueSpec* endSpec = mEndSpecs[j].get(); + MOZ_ASSERT(endSpec, "null SMILTimeValueSpec in list of end specs"); + endSpec->Unlink(); + } + + ClearIntervals(); + + // Make sure we don't notify other elements of new intervals + mTimeDependents.Clear(); +} + +//---------------------------------------------------------------------- +// Implementation helpers + +nsresult SMILTimedElement::SetBeginOrEndSpec(const nsAString& aSpec, + Element& aContextElement, + bool aIsBegin, + RemovalTestFunction aRemove) { + TimeValueSpecList& timeSpecsList = aIsBegin ? mBeginSpecs : mEndSpecs; + InstanceTimeList& instances = aIsBegin ? mBeginInstances : mEndInstances; + + ClearSpecs(timeSpecsList, instances, aRemove); + + AutoIntervalUpdateBatcher updateBatcher(*this); + + nsCharSeparatedTokenizer tokenizer(aSpec, ';'); + if (!tokenizer.hasMoreTokens()) { // Empty list + return NS_ERROR_FAILURE; + } + + bool hadFailure = false; + while (tokenizer.hasMoreTokens()) { + auto spec = MakeUnique<SMILTimeValueSpec>(*this, aIsBegin); + nsresult rv = spec->SetSpec(tokenizer.nextToken(), aContextElement); + if (NS_SUCCEEDED(rv)) { + timeSpecsList.AppendElement(std::move(spec)); + } else { + hadFailure = true; + } + } + + // The return value from this function is only used to determine if we should + // print a console message or not, so we return failure if we had one or more + // failures but we don't need to differentiate between different types of + // failures or the number of failures. + return hadFailure ? NS_ERROR_FAILURE : NS_OK; +} + +namespace { +// Adaptor functor for RemoveInstanceTimes that allows us to use function +// pointers instead. +// Without this we'd have to either templatize ClearSpecs and all its callers +// or pass bool flags around to specify which removal function to use here. +class MOZ_STACK_CLASS RemoveByFunction { + public: + explicit RemoveByFunction(SMILTimedElement::RemovalTestFunction aFunction) + : mFunction(aFunction) {} + bool operator()(SMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) { + return mFunction(aInstanceTime); + } + + private: + SMILTimedElement::RemovalTestFunction mFunction; +}; +} // namespace + +void SMILTimedElement::ClearSpecs(TimeValueSpecList& aSpecs, + InstanceTimeList& aInstances, + RemovalTestFunction aRemove) { + AutoIntervalUpdateBatcher updateBatcher(*this); + + for (uint32_t i = 0; i < aSpecs.Length(); ++i) { + aSpecs[i]->Unlink(); + } + aSpecs.Clear(); + + RemoveByFunction removeByFunction(aRemove); + RemoveInstanceTimes(aInstances, removeByFunction); +} + +void SMILTimedElement::ClearIntervals() { + if (mElementState != STATE_STARTUP) { + mElementState = STATE_POSTACTIVE; + } + mCurrentRepeatIteration = 0; + ResetCurrentInterval(); + + // Remove old intervals + for (int32_t i = mOldIntervals.Length() - 1; i >= 0; --i) { + mOldIntervals[i]->Unlink(); + } + mOldIntervals.Clear(); +} + +bool SMILTimedElement::ApplyEarlyEnd(const SMILTimeValue& aSampleTime) { + // This should only be called within DoSampleAt as a helper function + MOZ_ASSERT(mElementState == STATE_ACTIVE, + "Unexpected state to try to apply an early end"); + + bool updated = false; + + // Only apply an early end if we're not already ending. + if (mCurrentInterval->End()->Time() > aSampleTime) { + SMILInstanceTime* earlyEnd = CheckForEarlyEnd(aSampleTime); + if (earlyEnd) { + if (earlyEnd->IsDependent()) { + // Generate a new instance time for the early end since the + // existing instance time is part of some dependency chain that we + // don't want to participate in. + RefPtr<SMILInstanceTime> newEarlyEnd = + new SMILInstanceTime(earlyEnd->Time()); + mCurrentInterval->SetEnd(*newEarlyEnd); + } else { + mCurrentInterval->SetEnd(*earlyEnd); + } + updated = true; + } + } + return updated; +} + +namespace { +class MOZ_STACK_CLASS RemoveReset { + public: + explicit RemoveReset(const SMILInstanceTime* aCurrentIntervalBegin) + : mCurrentIntervalBegin(aCurrentIntervalBegin) {} + bool operator()(SMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) { + // SMIL 3.0 section 5.4.3, 'Resetting element state': + // Any instance times associated with past Event-values, Repeat-values, + // Accesskey-values or added via DOM method calls are removed from the + // dependent begin and end instance times lists. In effect, all events + // and DOM methods calls in the past are cleared. This does not apply to + // an instance time that defines the begin of the current interval. + return aInstanceTime->IsDynamic() && !aInstanceTime->ShouldPreserve() && + (!mCurrentIntervalBegin || aInstanceTime != mCurrentIntervalBegin); + } + + private: + const SMILInstanceTime* mCurrentIntervalBegin; +}; +} // namespace + +void SMILTimedElement::Reset() { + RemoveReset resetBegin(mCurrentInterval ? mCurrentInterval->Begin() + : nullptr); + RemoveInstanceTimes(mBeginInstances, resetBegin); + + RemoveReset resetEnd(nullptr); + RemoveInstanceTimes(mEndInstances, resetEnd); +} + +void SMILTimedElement::ClearTimingState(RemovalTestFunction aRemove) { + mElementState = STATE_STARTUP; + ClearIntervals(); + + UnsetBeginSpec(aRemove); + UnsetEndSpec(aRemove); + + if (mClient) { + mClient->Inactivate(false); + } +} + +void SMILTimedElement::RebuildTimingState(RemovalTestFunction aRemove) { + MOZ_ASSERT(mAnimationElement, + "Attempting to enable a timed element not attached to an " + "animation element"); + MOZ_ASSERT(mElementState == STATE_STARTUP, + "Rebuilding timing state from non-startup state"); + + if (mAnimationElement->HasAttr(nsGkAtoms::begin)) { + nsAutoString attValue; + mAnimationElement->GetAttr(nsGkAtoms::begin, attValue); + SetBeginSpec(attValue, *mAnimationElement, aRemove); + } + + if (mAnimationElement->HasAttr(nsGkAtoms::end)) { + nsAutoString attValue; + mAnimationElement->GetAttr(nsGkAtoms::end, attValue); + SetEndSpec(attValue, *mAnimationElement, aRemove); + } + + mPrevRegisteredMilestone = sMaxMilestone; + RegisterMilestone(); +} + +void SMILTimedElement::DoPostSeek() { + // Finish backwards seek + if (mSeekState == SEEK_BACKWARD_FROM_INACTIVE || + mSeekState == SEEK_BACKWARD_FROM_ACTIVE) { + // Previously some dynamic instance times may have been marked to be + // preserved because they were endpoints of an historic interval (which may + // or may not have been filtered). Now that we've finished a seek we should + // clear that flag for those instance times whose intervals are no longer + // historic. + UnpreserveInstanceTimes(mBeginInstances); + UnpreserveInstanceTimes(mEndInstances); + + // Now that the times have been unmarked perform a reset. This might seem + // counter-intuitive when we're only doing a seek within an interval but + // SMIL seems to require this. SMIL 3.0, 'Hyperlinks and timing': + // Resolved end times associated with events, Repeat-values, + // Accesskey-values or added via DOM method calls are cleared when seeking + // to time earlier than the resolved end time. + Reset(); + UpdateCurrentInterval(); + } + + switch (mSeekState) { + case SEEK_FORWARD_FROM_ACTIVE: + case SEEK_BACKWARD_FROM_ACTIVE: + if (mElementState != STATE_ACTIVE) { + FireTimeEventAsync(eSMILEndEvent, 0); + } + break; + + case SEEK_FORWARD_FROM_INACTIVE: + case SEEK_BACKWARD_FROM_INACTIVE: + if (mElementState == STATE_ACTIVE) { + FireTimeEventAsync(eSMILBeginEvent, 0); + } + break; + + case SEEK_NOT_SEEKING: + /* Do nothing */ + break; + } + + mSeekState = SEEK_NOT_SEEKING; +} + +void SMILTimedElement::UnpreserveInstanceTimes(InstanceTimeList& aList) { + const SMILInterval* prevInterval = GetPreviousInterval(); + const SMILInstanceTime* cutoff = mCurrentInterval ? mCurrentInterval->Begin() + : prevInterval ? prevInterval->Begin() + : nullptr; + uint32_t count = aList.Length(); + for (uint32_t i = 0; i < count; ++i) { + SMILInstanceTime* instance = aList[i].get(); + if (!cutoff || cutoff->Time().CompareTo(instance->Time()) < 0) { + instance->UnmarkShouldPreserve(); + } + } +} + +void SMILTimedElement::FilterHistory() { + // We should filter the intervals first, since instance times still used in an + // interval won't be filtered. + FilterIntervals(); + FilterInstanceTimes(mBeginInstances); + FilterInstanceTimes(mEndInstances); +} + +void SMILTimedElement::FilterIntervals() { + // We can filter old intervals that: + // + // a) are not the previous interval; AND + // b) are not in the middle of a dependency chain; AND + // c) are not the first interval + // + // Condition (a) is necessary since the previous interval is used for applying + // fill effects and updating the current interval. + // + // Condition (b) is necessary since even if this interval itself is not + // active, it may be part of a dependency chain that includes active + // intervals. Such chains are used to establish priorities within the + // animation sandwich. + // + // Condition (c) is necessary to support hyperlinks that target animations + // since in some cases the defined behavior is to seek the document back to + // the first resolved begin time. Presumably the intention here is not + // actually to use the first resolved begin time, the + // _the_first_resolved_begin_time_that_produced_an_interval. That is, + // if we have begin="-5s; -3s; 1s; 3s" with a duration on 1s, we should seek + // to 1s. The spec doesn't say this but I'm pretty sure that is the intention. + // It seems negative times were simply not considered. + // + // Although the above conditions allow us to safely filter intervals for most + // scenarios they do not cover all cases and there will still be scenarios + // that generate intervals indefinitely. In such a case we simply set + // a maximum number of intervals and drop any intervals beyond that threshold. + + uint32_t threshold = mOldIntervals.Length() > sMaxNumIntervals + ? mOldIntervals.Length() - sMaxNumIntervals + : 0; + IntervalList filteredList; + for (uint32_t i = 0; i < mOldIntervals.Length(); ++i) { + SMILInterval* interval = mOldIntervals[i].get(); + if (i != 0 && /*skip first interval*/ + i + 1 < mOldIntervals.Length() && /*skip previous interval*/ + (i < threshold || !interval->IsDependencyChainLink())) { + interval->Unlink(true /*filtered, not deleted*/); + } else { + filteredList.AppendElement(std::move(mOldIntervals[i])); + } + } + mOldIntervals = std::move(filteredList); +} + +namespace { +class MOZ_STACK_CLASS RemoveFiltered { + public: + explicit RemoveFiltered(SMILTimeValue aCutoff) : mCutoff(aCutoff) {} + bool operator()(SMILInstanceTime* aInstanceTime, uint32_t /*aIndex*/) { + // We can filter instance times that: + // a) Precede the end point of the previous interval; AND + // b) Are NOT syncbase times that might be updated to a time after the end + // point of the previous interval; AND + // c) Are NOT fixed end points in any remaining interval. + return aInstanceTime->Time() < mCutoff && aInstanceTime->IsFixedTime() && + !aInstanceTime->ShouldPreserve(); + } + + private: + SMILTimeValue mCutoff; +}; + +class MOZ_STACK_CLASS RemoveBelowThreshold { + public: + RemoveBelowThreshold(uint32_t aThreshold, + nsTArray<const SMILInstanceTime*>& aTimesToKeep) + : mThreshold(aThreshold), mTimesToKeep(aTimesToKeep) {} + bool operator()(SMILInstanceTime* aInstanceTime, uint32_t aIndex) { + return aIndex < mThreshold && !mTimesToKeep.Contains(aInstanceTime); + } + + private: + uint32_t mThreshold; + nsTArray<const SMILInstanceTime*>& mTimesToKeep; +}; +} // namespace + +void SMILTimedElement::FilterInstanceTimes(InstanceTimeList& aList) { + if (GetPreviousInterval()) { + RemoveFiltered removeFiltered(GetPreviousInterval()->End()->Time()); + RemoveInstanceTimes(aList, removeFiltered); + } + + // As with intervals it is possible to create a document that, even despite + // our most aggressive filtering, will generate instance times indefinitely + // (e.g. cyclic dependencies with TimeEvents---we can't filter such times as + // they're unpredictable due to the possibility of seeking the document which + // may prevent some events from being generated). Therefore we introduce + // a hard cutoff at which point we just drop the oldest instance times. + if (aList.Length() > sMaxNumInstanceTimes) { + uint32_t threshold = aList.Length() - sMaxNumInstanceTimes; + // There are a few instance times we should keep though, notably: + // - the current interval begin time, + // - the previous interval end time (see note in RemoveInstanceTimes) + // - the first interval begin time (see note in FilterIntervals) + nsTArray<const SMILInstanceTime*> timesToKeep; + if (mCurrentInterval) { + timesToKeep.AppendElement(mCurrentInterval->Begin()); + } + const SMILInterval* prevInterval = GetPreviousInterval(); + if (prevInterval) { + timesToKeep.AppendElement(prevInterval->End()); + } + if (!mOldIntervals.IsEmpty()) { + timesToKeep.AppendElement(mOldIntervals[0]->Begin()); + } + RemoveBelowThreshold removeBelowThreshold(threshold, timesToKeep); + RemoveInstanceTimes(aList, removeBelowThreshold); + } +} + +// +// This method is based on the pseudocode given in the SMILANIM spec. +// +// See: +// http://www.w3.org/TR/2001/REC-smil-animation-20010904/#Timing-BeginEnd-LC-Start +// +bool SMILTimedElement::GetNextInterval(const SMILInterval* aPrevInterval, + const SMILInterval* aReplacedInterval, + const SMILInstanceTime* aFixedBeginTime, + SMILInterval& aResult) const { + MOZ_ASSERT(!aFixedBeginTime || aFixedBeginTime->Time().IsDefinite(), + "Unresolved or indefinite begin time given for interval start"); + static const SMILTimeValue zeroTime(0L); + + if (mRestartMode == RESTART_NEVER && aPrevInterval) return false; + + // Calc starting point + SMILTimeValue beginAfter; + bool prevIntervalWasZeroDur = false; + if (aPrevInterval) { + beginAfter = aPrevInterval->End()->Time(); + prevIntervalWasZeroDur = + aPrevInterval->End()->Time() == aPrevInterval->Begin()->Time(); + } else { + beginAfter.SetMillis(INT64_MIN); + } + + RefPtr<SMILInstanceTime> tempBegin; + RefPtr<SMILInstanceTime> tempEnd; + + while (true) { + // Calculate begin time + if (aFixedBeginTime) { + if (aFixedBeginTime->Time() < beginAfter) { + return false; + } + // our ref-counting is not const-correct + tempBegin = const_cast<SMILInstanceTime*>(aFixedBeginTime); + } else if ((!mAnimationElement || + !mAnimationElement->HasAttr(nsGkAtoms::begin)) && + beginAfter <= zeroTime) { + tempBegin = new SMILInstanceTime(SMILTimeValue(0)); + } else { + int32_t beginPos = 0; + do { + tempBegin = + GetNextGreaterOrEqual(mBeginInstances, beginAfter, beginPos); + if (!tempBegin || !tempBegin->Time().IsDefinite()) { + return false; + } + // If we're updating the current interval then skip any begin time that + // is dependent on the current interval's begin time. e.g. + // <animate id="a" begin="b.begin; a.begin+2s"... + // If b's interval disappears whilst 'a' is in the waiting state the + // begin time at "a.begin+2s" should be skipped since 'a' never begun. + } while (aReplacedInterval && + tempBegin->GetBaseTime() == aReplacedInterval->Begin()); + } + MOZ_ASSERT(tempBegin && tempBegin->Time().IsDefinite() && + tempBegin->Time() >= beginAfter, + "Got a bad begin time while fetching next interval"); + + // Calculate end time + { + int32_t endPos = 0; + do { + tempEnd = + GetNextGreaterOrEqual(mEndInstances, tempBegin->Time(), endPos); + + // SMIL doesn't allow for coincident zero-duration intervals, so if the + // previous interval was zero-duration, and tempEnd is going to give us + // another zero duration interval, then look for another end to use + // instead. + if (tempEnd && prevIntervalWasZeroDur && + tempEnd->Time() == beginAfter) { + tempEnd = GetNextGreater(mEndInstances, tempBegin->Time(), endPos); + } + // As above with begin times, avoid creating self-referential loops + // between instance times by checking that the newly found end instance + // time is not already dependent on the end of the current interval. + } while (tempEnd && aReplacedInterval && + tempEnd->GetBaseTime() == aReplacedInterval->End()); + + if (!tempEnd) { + // If all the ends are before the beginning we have a bad interval + // UNLESS: + // a) We never had any end attribute to begin with (the SMIL pseudocode + // places this condition earlier in the flow but that fails to allow + // for DOM calls when no "indefinite" condition is given), OR + // b) We never had any end instance times to begin with, OR + // c) We have end events which leave the interval open-ended. + bool openEndedIntervalOk = mEndSpecs.IsEmpty() || + mEndInstances.IsEmpty() || + EndHasEventConditions(); + + // The above conditions correspond with the SMIL pseudocode but SMIL + // doesn't address self-dependent instance times which we choose to + // ignore. + // + // Therefore we add a qualification of (b) above that even if + // there are end instance times but they all depend on the end of the + // current interval we should act as if they didn't exist and allow the + // open-ended interval. + // + // In the following condition we don't use |= because it doesn't provide + // short-circuit behavior. + openEndedIntervalOk = + openEndedIntervalOk || + (aReplacedInterval && + AreEndTimesDependentOn(aReplacedInterval->End())); + + if (!openEndedIntervalOk) { + return false; // Bad interval + } + } + + SMILTimeValue intervalEnd = tempEnd ? tempEnd->Time() : SMILTimeValue(); + SMILTimeValue activeEnd = CalcActiveEnd(tempBegin->Time(), intervalEnd); + + if (!tempEnd || intervalEnd != activeEnd) { + tempEnd = new SMILInstanceTime(activeEnd); + } + } + MOZ_ASSERT(tempEnd, "Failed to get end point for next interval"); + + // When we choose the interval endpoints, we don't allow coincident + // zero-duration intervals, so if we arrive here and we have a zero-duration + // interval starting at the same point as a previous zero-duration interval, + // then it must be because we've applied constraints to the active duration. + // In that case, we will potentially run into an infinite loop, so we break + // it by searching for the next interval that starts AFTER our current + // zero-duration interval. + if (prevIntervalWasZeroDur && tempEnd->Time() == beginAfter) { + beginAfter.SetMillis(tempBegin->Time().GetMillis() + 1); + prevIntervalWasZeroDur = false; + continue; + } + prevIntervalWasZeroDur = tempBegin->Time() == tempEnd->Time(); + + // Check for valid interval + if (tempEnd->Time() > zeroTime || + (tempBegin->Time() == zeroTime && tempEnd->Time() == zeroTime)) { + aResult.Set(*tempBegin, *tempEnd); + return true; + } + + if (mRestartMode == RESTART_NEVER) { + // tempEnd <= 0 so we're going to loop which effectively means restarting + return false; + } + + beginAfter = tempEnd->Time(); + } + MOZ_ASSERT_UNREACHABLE("Hmm... we really shouldn't be here"); + + return false; +} + +SMILInstanceTime* SMILTimedElement::GetNextGreater( + const InstanceTimeList& aList, const SMILTimeValue& aBase, + int32_t& aPosition) const { + SMILInstanceTime* result = nullptr; + while ((result = GetNextGreaterOrEqual(aList, aBase, aPosition)) && + result->Time() == aBase) { + } + return result; +} + +SMILInstanceTime* SMILTimedElement::GetNextGreaterOrEqual( + const InstanceTimeList& aList, const SMILTimeValue& aBase, + int32_t& aPosition) const { + SMILInstanceTime* result = nullptr; + int32_t count = aList.Length(); + + for (; aPosition < count && !result; ++aPosition) { + SMILInstanceTime* val = aList[aPosition].get(); + MOZ_ASSERT(val, "NULL instance time in list"); + if (val->Time() >= aBase) { + result = val; + } + } + + return result; +} + +/** + * @see SMILANIM 3.3.4 + */ +SMILTimeValue SMILTimedElement::CalcActiveEnd(const SMILTimeValue& aBegin, + const SMILTimeValue& aEnd) const { + SMILTimeValue result; + + MOZ_ASSERT(mSimpleDur.IsResolved(), + "Unresolved simple duration in CalcActiveEnd"); + MOZ_ASSERT(aBegin.IsDefinite(), + "Indefinite or unresolved begin time in CalcActiveEnd"); + + result = GetRepeatDuration(); + + if (aEnd.IsDefinite()) { + SMILTime activeDur = aEnd.GetMillis() - aBegin.GetMillis(); + + if (result.IsDefinite()) { + result.SetMillis(std::min(result.GetMillis(), activeDur)); + } else { + result.SetMillis(activeDur); + } + } + + result = ApplyMinAndMax(result); + + if (result.IsDefinite()) { + SMILTime activeEnd = result.GetMillis() + aBegin.GetMillis(); + result.SetMillis(activeEnd); + } + + return result; +} + +SMILTimeValue SMILTimedElement::GetRepeatDuration() const { + SMILTimeValue multipliedDuration; + if (mRepeatCount.IsDefinite() && mSimpleDur.IsDefinite()) { + if (mRepeatCount * double(mSimpleDur.GetMillis()) < + double(std::numeric_limits<SMILTime>::max())) { + multipliedDuration.SetMillis( + SMILTime(mRepeatCount * mSimpleDur.GetMillis())); + } + } else { + multipliedDuration.SetIndefinite(); + } + + SMILTimeValue repeatDuration; + + if (mRepeatDur.IsResolved()) { + repeatDuration = std::min(multipliedDuration, mRepeatDur); + } else if (mRepeatCount.IsSet()) { + repeatDuration = multipliedDuration; + } else { + repeatDuration = mSimpleDur; + } + + return repeatDuration; +} + +SMILTimeValue SMILTimedElement::ApplyMinAndMax( + const SMILTimeValue& aDuration) const { + if (!aDuration.IsResolved()) { + return aDuration; + } + + if (mMax < mMin) { + return aDuration; + } + + SMILTimeValue result; + + if (aDuration > mMax) { + result = mMax; + } else if (aDuration < mMin) { + result = mMin; + } else { + result = aDuration; + } + + return result; +} + +SMILTime SMILTimedElement::ActiveTimeToSimpleTime(SMILTime aActiveTime, + uint32_t& aRepeatIteration) { + SMILTime result; + + MOZ_ASSERT(mSimpleDur.IsResolved(), + "Unresolved simple duration in ActiveTimeToSimpleTime"); + MOZ_ASSERT(aActiveTime >= 0, "Expecting non-negative active time"); + // Note that a negative aActiveTime will give us a negative value for + // aRepeatIteration, which is bad because aRepeatIteration is unsigned + + if (mSimpleDur.IsIndefinite() || mSimpleDur.GetMillis() == 0L) { + aRepeatIteration = 0; + result = aActiveTime; + } else { + result = aActiveTime % mSimpleDur.GetMillis(); + aRepeatIteration = (uint32_t)(aActiveTime / mSimpleDur.GetMillis()); + } + + return result; +} + +// +// Although in many cases it would be possible to check for an early end and +// adjust the current interval well in advance the SMIL Animation spec seems to +// indicate that we should only apply an early end at the latest possible +// moment. In particular, this paragraph from section 3.6.8: +// +// 'If restart is set to "always", then the current interval will end early if +// there is an instance time in the begin list that is before (i.e. earlier +// than) the defined end for the current interval. Ending in this manner will +// also send a changed time notice to all time dependents for the current +// interval end.' +// +SMILInstanceTime* SMILTimedElement::CheckForEarlyEnd( + const SMILTimeValue& aContainerTime) const { + MOZ_ASSERT(mCurrentInterval, + "Checking for an early end but the current interval is not set"); + if (mRestartMode != RESTART_ALWAYS) return nullptr; + + int32_t position = 0; + SMILInstanceTime* nextBegin = GetNextGreater( + mBeginInstances, mCurrentInterval->Begin()->Time(), position); + + if (nextBegin && nextBegin->Time() > mCurrentInterval->Begin()->Time() && + nextBegin->Time() < mCurrentInterval->End()->Time() && + nextBegin->Time() <= aContainerTime) { + return nextBegin; + } + + return nullptr; +} + +void SMILTimedElement::UpdateCurrentInterval(bool aForceChangeNotice) { + // Check if updates are currently blocked (batched) + if (mDeferIntervalUpdates) { + mDoDeferredUpdate = true; + return; + } + + // We adopt the convention of not resolving intervals until the first + // sample. Otherwise, every time each attribute is set we'll re-resolve the + // current interval and notify all our time dependents of the change. + // + // The disadvantage of deferring resolving the interval is that DOM calls to + // to getStartTime will throw an INVALID_STATE_ERR exception until the + // document timeline begins since the start time has not yet been resolved. + if (mElementState == STATE_STARTUP) return; + + // Although SMIL gives rules for detecting cycles in change notifications, + // some configurations can lead to create-delete-create-delete-etc. cycles + // which SMIL does not consider. + // + // In order to provide consistent behavior in such cases, we detect two + // deletes in a row and then refuse to create any further intervals. That is, + // we say the configuration is invalid. + if (mDeleteCount > 1) { + // When we update the delete count we also set the state to post active, so + // if we're not post active here then something other than + // UpdateCurrentInterval has updated the element state in between and all + // bets are off. + MOZ_ASSERT(mElementState == STATE_POSTACTIVE, + "Expected to be in post-active state after performing double " + "delete"); + return; + } + + // Check that we aren't stuck in infinite recursion updating some syncbase + // dependencies. Generally such situations should be detected in advance and + // the chain broken in a sensible and predictable manner, so if we're hitting + // this assertion we need to work out how to detect the case that's causing + // it. In release builds, just bail out before we overflow the stack. + AutoRestore<uint8_t> depthRestorer(mUpdateIntervalRecursionDepth); + if (++mUpdateIntervalRecursionDepth > sMaxUpdateIntervalRecursionDepth) { + MOZ_ASSERT(false, + "Update current interval recursion depth exceeded threshold"); + return; + } + + // If the interval is active the begin time is fixed. + const SMILInstanceTime* beginTime = + mElementState == STATE_ACTIVE ? mCurrentInterval->Begin() : nullptr; + SMILInterval updatedInterval; + if (GetNextInterval(GetPreviousInterval(), mCurrentInterval.get(), beginTime, + updatedInterval)) { + if (mElementState == STATE_POSTACTIVE) { + MOZ_ASSERT(!mCurrentInterval, + "In postactive state but the interval has been set"); + mCurrentInterval = MakeUnique<SMILInterval>(updatedInterval); + mElementState = STATE_WAITING; + NotifyNewInterval(); + + } else { + bool beginChanged = false; + bool endChanged = false; + + if (mElementState != STATE_ACTIVE && + !updatedInterval.Begin()->SameTimeAndBase( + *mCurrentInterval->Begin())) { + mCurrentInterval->SetBegin(*updatedInterval.Begin()); + beginChanged = true; + } + + if (!updatedInterval.End()->SameTimeAndBase(*mCurrentInterval->End())) { + mCurrentInterval->SetEnd(*updatedInterval.End()); + endChanged = true; + } + + if (beginChanged || endChanged || aForceChangeNotice) { + NotifyChangedInterval(mCurrentInterval.get(), beginChanged, endChanged); + } + } + + // There's a chance our next milestone has now changed, so update the time + // container + RegisterMilestone(); + } else { // GetNextInterval failed: Current interval is no longer valid + if (mElementState == STATE_ACTIVE) { + // The interval is active so we can't just delete it, instead trim it so + // that begin==end. + if (!mCurrentInterval->End()->SameTimeAndBase( + *mCurrentInterval->Begin())) { + mCurrentInterval->SetEnd(*mCurrentInterval->Begin()); + NotifyChangedInterval(mCurrentInterval.get(), false, true); + } + // The transition to the postactive state will take place on the next + // sample (along with firing end events, clearing intervals etc.) + RegisterMilestone(); + } else if (mElementState == STATE_WAITING) { + AutoRestore<uint8_t> deleteCountRestorer(mDeleteCount); + ++mDeleteCount; + mElementState = STATE_POSTACTIVE; + ResetCurrentInterval(); + } + } +} + +void SMILTimedElement::SampleSimpleTime(SMILTime aActiveTime) { + if (mClient) { + uint32_t repeatIteration; + SMILTime simpleTime = ActiveTimeToSimpleTime(aActiveTime, repeatIteration); + mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration); + } +} + +void SMILTimedElement::SampleFillValue() { + if (mFillMode != FILL_FREEZE || !mClient) return; + + SMILTime activeTime; + + if (mElementState == STATE_WAITING || mElementState == STATE_POSTACTIVE) { + const SMILInterval* prevInterval = GetPreviousInterval(); + MOZ_ASSERT(prevInterval, + "Attempting to sample fill value but there is no previous " + "interval"); + MOZ_ASSERT(prevInterval->End()->Time().IsDefinite() && + prevInterval->End()->IsFixedTime(), + "Attempting to sample fill value but the endpoint of the " + "previous interval is not resolved and fixed"); + + activeTime = prevInterval->End()->Time().GetMillis() - + prevInterval->Begin()->Time().GetMillis(); + + // If the interval's repeat duration was shorter than its active duration, + // use the end of the repeat duration to determine the frozen animation's + // state. + SMILTimeValue repeatDuration = GetRepeatDuration(); + if (repeatDuration.IsDefinite()) { + activeTime = std::min(repeatDuration.GetMillis(), activeTime); + } + } else { + MOZ_ASSERT( + mElementState == STATE_ACTIVE, + "Attempting to sample fill value when we're in an unexpected state " + "(probably STATE_STARTUP)"); + + // If we are being asked to sample the fill value while active we *must* + // have a repeat duration shorter than the active duration so use that. + MOZ_ASSERT(GetRepeatDuration().IsDefinite(), + "Attempting to sample fill value of an active animation with " + "an indefinite repeat duration"); + activeTime = GetRepeatDuration().GetMillis(); + } + + uint32_t repeatIteration; + SMILTime simpleTime = ActiveTimeToSimpleTime(activeTime, repeatIteration); + + if (simpleTime == 0L && repeatIteration) { + mClient->SampleLastValue(--repeatIteration); + } else { + mClient->SampleAt(simpleTime, mSimpleDur, repeatIteration); + } +} + +nsresult SMILTimedElement::AddInstanceTimeFromCurrentTime(SMILTime aCurrentTime, + double aOffsetSeconds, + bool aIsBegin) { + double offset = NS_round(aOffsetSeconds * PR_MSEC_PER_SEC); + + // Check we won't overflow the range of SMILTime + if (aCurrentTime + offset > double(std::numeric_limits<SMILTime>::max())) + return NS_ERROR_ILLEGAL_VALUE; + + SMILTimeValue timeVal(aCurrentTime + int64_t(offset)); + + RefPtr<SMILInstanceTime> instanceTime = + new SMILInstanceTime(timeVal, SMILInstanceTime::SOURCE_DOM); + + AddInstanceTime(instanceTime, aIsBegin); + + return NS_OK; +} + +void SMILTimedElement::RegisterMilestone() { + SMILTimeContainer* container = GetTimeContainer(); + if (!container) return; + MOZ_ASSERT(mAnimationElement, + "Got a time container without an owning animation element"); + + SMILMilestone nextMilestone; + if (!GetNextMilestone(nextMilestone)) return; + + // This method is called every time we might possibly have updated our + // current interval, but since SMILTimeContainer makes no attempt to filter + // out redundant milestones we do some rudimentary filtering here. It's not + // perfect, but unnecessary samples are fairly cheap. + if (nextMilestone >= mPrevRegisteredMilestone) return; + + container->AddMilestone(nextMilestone, *mAnimationElement); + mPrevRegisteredMilestone = nextMilestone; +} + +bool SMILTimedElement::GetNextMilestone(SMILMilestone& aNextMilestone) const { + // Return the next key moment in our lifetime. + // + // XXX It may be possible in future to optimise this so that we only register + // for milestones if: + // a) We have time dependents, or + // b) We are dependent on events or syncbase relationships, or + // c) There are registered listeners for our events + // + // Then for the simple case where everything uses offset values we could + // ignore milestones altogether. + // + // We'd need to be careful, however, that if one of those conditions became + // true in between samples that we registered our next milestone at that + // point. + + switch (mElementState) { + case STATE_STARTUP: + // All elements register for an initial end sample at t=0 where we resolve + // our initial interval. + aNextMilestone.mIsEnd = true; // Initial sample should be an end sample + aNextMilestone.mTime = 0; + return true; + + case STATE_WAITING: + MOZ_ASSERT(mCurrentInterval, + "In waiting state but the current interval has not been set"); + aNextMilestone.mIsEnd = false; + aNextMilestone.mTime = mCurrentInterval->Begin()->Time().GetMillis(); + return true; + + case STATE_ACTIVE: { + // Work out what comes next: the interval end or the next repeat iteration + SMILTimeValue nextRepeat; + if (mSeekState == SEEK_NOT_SEEKING && mSimpleDur.IsDefinite()) { + SMILTime nextRepeatActiveTime = + (mCurrentRepeatIteration + 1) * mSimpleDur.GetMillis(); + // Check that the repeat fits within the repeat duration + if (SMILTimeValue(nextRepeatActiveTime) < GetRepeatDuration()) { + nextRepeat.SetMillis(mCurrentInterval->Begin()->Time().GetMillis() + + nextRepeatActiveTime); + } + } + SMILTimeValue nextMilestone = + std::min(mCurrentInterval->End()->Time(), nextRepeat); + + // Check for an early end before that time + SMILInstanceTime* earlyEnd = CheckForEarlyEnd(nextMilestone); + if (earlyEnd) { + aNextMilestone.mIsEnd = true; + aNextMilestone.mTime = earlyEnd->Time().GetMillis(); + return true; + } + + // Apply the previously calculated milestone + if (nextMilestone.IsDefinite()) { + aNextMilestone.mIsEnd = nextMilestone != nextRepeat; + aNextMilestone.mTime = nextMilestone.GetMillis(); + return true; + } + + return false; + } + + case STATE_POSTACTIVE: + return false; + } + MOZ_CRASH("Invalid element state"); +} + +void SMILTimedElement::NotifyNewInterval() { + MOZ_ASSERT(mCurrentInterval, + "Attempting to notify dependents of a new interval but the " + "interval is not set"); + + SMILTimeContainer* container = GetTimeContainer(); + if (container) { + container->SyncPauseTime(); + } + + for (SMILTimeValueSpec* spec : mTimeDependents.Keys()) { + SMILInterval* interval = mCurrentInterval.get(); + // It's possible that in notifying one new time dependent of a new interval + // that a chain reaction is triggered which results in the original + // interval disappearing. If that's the case we can skip sending further + // notifications. + if (!interval) { + break; + } + spec->HandleNewInterval(*interval, container); + } +} + +void SMILTimedElement::NotifyChangedInterval(SMILInterval* aInterval, + bool aBeginObjectChanged, + bool aEndObjectChanged) { + MOZ_ASSERT(aInterval, "Null interval for change notification"); + + SMILTimeContainer* container = GetTimeContainer(); + if (container) { + container->SyncPauseTime(); + } + + // Copy the instance times list since notifying the instance times can result + // in a chain reaction whereby our own interval gets deleted along with its + // instance times. + InstanceTimeList times; + aInterval->GetDependentTimes(times); + + for (uint32_t i = 0; i < times.Length(); ++i) { + times[i]->HandleChangedInterval(container, aBeginObjectChanged, + aEndObjectChanged); + } +} + +void SMILTimedElement::FireTimeEventAsync(EventMessage aMsg, int32_t aDetail) { + if (!mAnimationElement) return; + + nsCOMPtr<nsIRunnable> event = + new AsyncTimeEventRunner(mAnimationElement, aMsg, aDetail); + mAnimationElement->OwnerDoc()->Dispatch(TaskCategory::Other, event.forget()); +} + +const SMILInstanceTime* SMILTimedElement::GetEffectiveBeginInstance() const { + switch (mElementState) { + case STATE_STARTUP: + return nullptr; + + case STATE_ACTIVE: + return mCurrentInterval->Begin(); + + case STATE_WAITING: + case STATE_POSTACTIVE: { + const SMILInterval* prevInterval = GetPreviousInterval(); + return prevInterval ? prevInterval->Begin() : nullptr; + } + } + MOZ_CRASH("Invalid element state"); +} + +const SMILInterval* SMILTimedElement::GetPreviousInterval() const { + return mOldIntervals.IsEmpty() + ? nullptr + : mOldIntervals[mOldIntervals.Length() - 1].get(); +} + +bool SMILTimedElement::HasClientInFillRange() const { + // Returns true if we have a client that is in the range where it will fill + return mClient && ((mElementState != STATE_ACTIVE && HasPlayed()) || + (mElementState == STATE_ACTIVE && !mClient->IsActive())); +} + +bool SMILTimedElement::EndHasEventConditions() const { + for (uint32_t i = 0; i < mEndSpecs.Length(); ++i) { + if (mEndSpecs[i]->IsEventBased()) return true; + } + return false; +} + +bool SMILTimedElement::AreEndTimesDependentOn( + const SMILInstanceTime* aBase) const { + if (mEndInstances.IsEmpty()) return false; + + for (uint32_t i = 0; i < mEndInstances.Length(); ++i) { + if (mEndInstances[i]->GetBaseTime() != aBase) { + return false; + } + } + return true; +} + +} // namespace mozilla diff --git a/dom/smil/SMILTimedElement.h b/dom/smil/SMILTimedElement.h new file mode 100644 index 0000000000..fc2792d1dc --- /dev/null +++ b/dom/smil/SMILTimedElement.h @@ -0,0 +1,649 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTIMEDELEMENT_H_ +#define DOM_SMIL_SMILTIMEDELEMENT_H_ + +#include <utility> + +#include "mozilla/EventForwards.h" +#include "mozilla/SMILInstanceTime.h" +#include "mozilla/SMILInterval.h" +#include "mozilla/SMILMilestone.h" +#include "mozilla/SMILRepeatCount.h" +#include "mozilla/SMILTimeValueSpec.h" +#include "mozilla/SMILTypes.h" +#include "mozilla/UniquePtr.h" +#include "nsAttrValue.h" +#include "nsHashKeys.h" +#include "nsTArray.h" +#include "nsTHashtable.h" + +class nsAtom; + +namespace mozilla { + +class SMILAnimationFunction; +class SMILTimeContainer; +class SMILTimeValue; + +namespace dom { +class SVGAnimationElement; +} // namespace dom + +//---------------------------------------------------------------------- +// SMILTimedElement + +class SMILTimedElement { + public: + SMILTimedElement(); + ~SMILTimedElement(); + + using Element = dom::Element; + + /* + * Sets the owning animation element which this class uses to convert between + * container times and to register timebase elements. + */ + void SetAnimationElement(mozilla::dom::SVGAnimationElement* aElement); + + /* + * Returns the time container with which this timed element is associated or + * nullptr if it is not associated with a time container. + */ + SMILTimeContainer* GetTimeContainer(); + + /* + * Returns the element targeted by the animation element. Needed for + * registering event listeners against the appropriate element. + */ + Element* GetTargetElement(); + + /** + * Methods for supporting the ElementTimeControl interface. + */ + + /* + * Adds a new begin instance time at the current container time plus or minus + * the specified offset. + * + * @param aOffsetSeconds A real number specifying the number of seconds to add + * to the current container time. + * @return NS_OK if the operation succeeeded, or an error code otherwise. + */ + nsresult BeginElementAt(double aOffsetSeconds); + + /* + * Adds a new end instance time at the current container time plus or minus + * the specified offset. + * + * @param aOffsetSeconds A real number specifying the number of seconds to add + * to the current container time. + * @return NS_OK if the operation succeeeded, or an error code otherwise. + */ + nsresult EndElementAt(double aOffsetSeconds); + + /** + * Methods for supporting the SVGAnimationElement interface. + */ + + /** + * According to SVG 1.1 SE this returns + * + * the begin time, in seconds, for this animation element's current + * interval, if it exists, regardless of whether the interval has begun yet. + * + * @return the start time as defined above in milliseconds or an unresolved + * time if there is no current interval. + */ + SMILTimeValue GetStartTime() const; + + /** + * Returns the simple duration of this element. + * + * @return the simple duration in milliseconds or INDEFINITE. + */ + SMILTimeValue GetSimpleDuration() const { return mSimpleDur; } + + /** + * Methods for supporting hyperlinking + */ + + /** + * Internal SMIL methods + */ + + /** + * Returns the time to seek the document to when this element is targetted by + * a hyperlink. + * + * The behavior is defined here: + * http://www.w3.org/TR/smil-animation/#HyperlinkSemantics + * + * It is very similar to GetStartTime() with the exception that when the + * element is not active, the begin time of the *first* interval is returned. + * + * @return the time to seek the documen to in milliseconds or an unresolved + * time if there is no resolved interval. + */ + SMILTimeValue GetHyperlinkTime() const; + + /** + * Adds an instance time object this element's list of instance times. + * These instance times are used when creating intervals. + * + * This method is typically called by an SMILTimeValueSpec. + * + * @param aInstanceTime The time to add, expressed in container time. + * @param aIsBegin true if the time to be added represents a begin + * time or false if it represents an end time. + */ + void AddInstanceTime(SMILInstanceTime* aInstanceTime, bool aIsBegin); + + /** + * Requests this element update the given instance time. + * + * This method is typically called by a child SMILTimeValueSpec. + * + * @param aInstanceTime The instance time to update. + * @param aUpdatedTime The time to update aInstanceTime with. + * @param aDependentTime The instance time upon which aInstanceTime should be + * based. + * @param aIsBegin true if the time to be updated represents a begin + * instance time or false if it represents an end + * instance time. + */ + void UpdateInstanceTime(SMILInstanceTime* aInstanceTime, + SMILTimeValue& aUpdatedTime, bool aIsBegin); + + /** + * Removes an instance time object from this element's list of instance times. + * + * This method is typically called by a child SMILTimeValueSpec. + * + * @param aInstanceTime The instance time to remove. + * @param aIsBegin true if the time to be removed represents a begin + * time or false if it represents an end time. + */ + void RemoveInstanceTime(SMILInstanceTime* aInstanceTime, bool aIsBegin); + + /** + * Removes all the instance times associated with the given + * SMILTimeValueSpec object. Used when an ID assignment changes and hence + * all the previously associated instance times become invalid. + * + * @param aCreator The SMILTimeValueSpec object whose created + * SMILInstanceTime's should be removed. + * @param aIsBegin true if the times to be removed represent begin + * times or false if they are end times. + */ + void RemoveInstanceTimesForCreator(const SMILTimeValueSpec* aCreator, + bool aIsBegin); + + /** + * Sets the object that will be called by this timed element each time it is + * sampled. + * + * In Schmitz's model it is possible to associate several time clients with + * a timed element but for now we only allow one. + * + * @param aClient The time client to associate. Any previous time client + * will be disassociated and no longer sampled. Setting this + * to nullptr will simply disassociate the previous client, + * if any. + */ + void SetTimeClient(SMILAnimationFunction* aClient); + + /** + * Samples the object at the given container time. Timing intervals are + * updated and if this element is active at the given time the associated time + * client will be sampled with the appropriate simple time. + * + * @param aContainerTime The container time at which to sample. + */ + void SampleAt(SMILTime aContainerTime); + + /** + * Performs a special sample for the end of an interval. Such a sample should + * only advance the timed element (and any dependent elements) to the waiting + * or postactive state. It should not cause a transition to the active state. + * Transition to the active state is only performed on a regular SampleAt. + * + * This allows all interval ends at a given time to be processed first and + * hence the new interval can be established based on full information of the + * available instance times. + * + * @param aContainerTime The container time at which to sample. + */ + void SampleEndAt(SMILTime aContainerTime); + + /** + * Informs the timed element that its time container has changed time + * relative to document time. The timed element therefore needs to update its + * dependent elements (which may belong to a different time container) so they + * can re-resolve their times. + */ + void HandleContainerTimeChange(); + + /** + * Resets this timed element's accumulated times and intervals back to start + * up state. + * + * This is used for backwards seeking where rather than accumulating + * historical timing state and winding it back, we reset the element and seek + * forwards. + */ + void Rewind(); + + /** + * Marks this element as disabled or not. If the element is disabled, it + * will ignore any future samples and discard any accumulated timing state. + * + * This is used by SVG to "turn off" timed elements when the associated + * animation element has failing conditional processing tests. + * + * Returns true if the disabled state of the timed element was changed + * as a result of this call (i.e. it was not a redundant call). + */ + bool SetIsDisabled(bool aIsDisabled); + + /** + * Attempts to set an attribute on this timed element. + * + * @param aAttribute The name of the attribute to set. The namespace of this + * attribute is not specified as it is checked by the host + * element. Only attributes in the namespace defined for + * SMIL attributes in the host language are passed to the + * timed element. + * @param aValue The attribute value. + * @param aResult The nsAttrValue object that may be used for storing the + * parsed result. + * @param aContextElement The element to use for context when resolving + * references to other elements. + * @param[out] aParseResult The result of parsing the attribute. Will be set + * to NS_OK if parsing is successful. + * + * @return true if the given attribute is a timing attribute, false + * otherwise. + */ + bool SetAttr(nsAtom* aAttribute, const nsAString& aValue, + nsAttrValue& aResult, Element& aContextElement, + nsresult* aParseResult = nullptr); + + /** + * Attempts to unset an attribute on this timed element. + * + * @param aAttribute The name of the attribute to set. As with SetAttr the + * namespace of the attribute is not specified (see + * SetAttr). + * + * @return true if the given attribute is a timing attribute, false + * otherwise. + */ + bool UnsetAttr(nsAtom* aAttribute); + + /** + * Adds a syncbase dependency to the list of dependents that will be notified + * when this timed element creates, deletes, or updates its current interval. + * + * @param aDependent The SMILTimeValueSpec object to notify. A raw pointer + * to this object will be stored. Therefore it is necessary + * for the object to be explicitly unregistered (with + * RemoveDependent) when it is destroyed. + */ + void AddDependent(SMILTimeValueSpec& aDependent); + + /** + * Removes a syncbase dependency from the list of dependents that are notified + * when the current interval is modified. + * + * @param aDependent The SMILTimeValueSpec object to unregister. + */ + void RemoveDependent(SMILTimeValueSpec& aDependent); + + /** + * Determines if this timed element is dependent on the given timed element's + * begin time for the interval currently in effect. Whilst the element is in + * the active state this is the current interval and in the postactive or + * waiting state this is the previous interval if one exists. In all other + * cases the element is not considered a time dependent of any other element. + * + * @param aOther The potential syncbase element. + * @return true if this timed element's begin time for the currently + * effective interval is directly or indirectly derived from aOther, false + * otherwise. + */ + bool IsTimeDependent(const SMILTimedElement& aOther) const; + + /** + * Called when the timed element has been bound to the document so that + * references from this timed element to other elements can be resolved. + * + * @param aContextElement The element which provides the necessary context for + * resolving references. This is typically the element + * in the host language that owns this timed element. + */ + void BindToTree(Element& aContextElement); + + /** + * Called when the target of the animation has changed so that event + * registrations can be updated. + */ + void HandleTargetElementChange(Element* aNewTarget); + + /** + * Called when the timed element has been removed from a document so that + * references to other elements can be broken. + */ + void DissolveReferences() { Unlink(); } + + // Cycle collection + void Traverse(nsCycleCollectionTraversalCallback* aCallback); + void Unlink(); + + using RemovalTestFunction = bool (*)(SMILInstanceTime* aInstance); + + protected: + // Typedefs + using TimeValueSpecList = nsTArray<UniquePtr<SMILTimeValueSpec>>; + using InstanceTimeList = nsTArray<RefPtr<SMILInstanceTime>>; + using IntervalList = nsTArray<UniquePtr<SMILInterval>>; + using TimeValueSpecPtrKey = nsPtrHashKey<SMILTimeValueSpec>; + using TimeValueSpecHashSet = nsTHashtable<TimeValueSpecPtrKey>; + + // Helper classes + class InstanceTimeComparator { + public: + bool Equals(const SMILInstanceTime* aElem1, + const SMILInstanceTime* aElem2) const; + bool LessThan(const SMILInstanceTime* aElem1, + const SMILInstanceTime* aElem2) const; + }; + + // Templated helper functions + template <class TestFunctor> + void RemoveInstanceTimes(InstanceTimeList& aArray, TestFunctor& aTest); + + // + // Implementation helpers + // + + nsresult SetBeginSpec(const nsAString& aBeginSpec, Element& aContextElement, + RemovalTestFunction aRemove); + nsresult SetEndSpec(const nsAString& aEndSpec, Element& aContextElement, + RemovalTestFunction aRemove); + nsresult SetSimpleDuration(const nsAString& aDurSpec); + nsresult SetMin(const nsAString& aMinSpec); + nsresult SetMax(const nsAString& aMaxSpec); + nsresult SetRestart(const nsAString& aRestartSpec); + nsresult SetRepeatCount(const nsAString& aRepeatCountSpec); + nsresult SetRepeatDur(const nsAString& aRepeatDurSpec); + nsresult SetFillMode(const nsAString& aFillModeSpec); + + void UnsetBeginSpec(RemovalTestFunction aRemove); + void UnsetEndSpec(RemovalTestFunction aRemove); + void UnsetSimpleDuration(); + void UnsetMin(); + void UnsetMax(); + void UnsetRestart(); + void UnsetRepeatCount(); + void UnsetRepeatDur(); + void UnsetFillMode(); + + nsresult SetBeginOrEndSpec(const nsAString& aSpec, Element& aContextElement, + bool aIsBegin, RemovalTestFunction aRemove); + void ClearSpecs(TimeValueSpecList& aSpecs, InstanceTimeList& aInstances, + RemovalTestFunction aRemove); + void ClearIntervals(); + void DoSampleAt(SMILTime aContainerTime, bool aEndOnly); + + /** + * Helper function to check for an early end and, if necessary, update the + * current interval accordingly. + * + * See SMIL 3.0, section 5.4.5, Element life cycle, "Active Time - Playing an + * interval" for a description of ending early. + * + * @param aSampleTime The current sample time. Early ends should only be + * applied at the last possible moment (i.e. if they are at + * or before the current sample time) and only if the + * current interval is not already ending. + * @return true if the end time of the current interval was updated, + * false otherwise. + */ + bool ApplyEarlyEnd(const SMILTimeValue& aSampleTime); + + /** + * Clears certain state in response to the element restarting. + * + * This state is described in SMIL 3.0, section 5.4.3, Resetting element state + */ + void Reset(); + + /** + * Clears all accumulated timing state except for those instance times for + * which aRemove does not return true. + * + * Unlike the Reset method which only clears instance times, this clears the + * element's state, intervals (including current interval), and tells the + * client animation function to stop applying a result. In effect, it returns + * the element to its initial state but preserves any instance times excluded + * by the passed-in function. + */ + void ClearTimingState(RemovalTestFunction aRemove); + + /** + * Recreates timing state by re-applying begin/end attributes specified on + * the associated animation element. + * + * Note that this does not completely restore the information cleared by + * ClearTimingState since it leaves the element in the startup state. + * The element state will be updated on the next sample. + */ + void RebuildTimingState(RemovalTestFunction aRemove); + + /** + * Completes a seek operation by sending appropriate events and, in the case + * of a backwards seek, updating the state of timing information that was + * previously considered historical. + */ + void DoPostSeek(); + + /** + * Unmarks instance times that were previously preserved because they were + * considered important historical milestones but are no longer such because + * a backwards seek has been performed. + */ + void UnpreserveInstanceTimes(InstanceTimeList& aList); + + /** + * Helper function to iterate through this element's accumulated timing + * information (specifically old SMILIntervals and SMILTimeInstanceTimes) + * and discard items that are no longer needed or exceed some threshold of + * accumulated state. + */ + void FilterHistory(); + + // Helper functions for FilterHistory to clear old SMILIntervals and + // SMILInstanceTimes respectively. + void FilterIntervals(); + void FilterInstanceTimes(InstanceTimeList& aList); + + /** + * Calculates the next acceptable interval for this element after the + * specified interval, or, if no previous interval is specified, it will be + * the first interval with an end time after t=0. + * + * @see SMILANIM 3.6.8 + * + * @param aPrevInterval The previous interval used. If supplied, the first + * interval that begins after aPrevInterval will be + * returned. May be nullptr. + * @param aReplacedInterval The interval that is being updated (if any). This + * used to ensure we don't return interval endpoints + * that are dependent on themselves. May be nullptr. + * @param aFixedBeginTime The time to use for the start of the interval. This + * is used when only the endpoint of the interval + * should be updated such as when the animation is in + * the ACTIVE state. May be nullptr. + * @param[out] aResult The next interval. Will be unchanged if no suitable + * interval was found (in which case false will be + * returned). + * @return true if a suitable interval was found, false otherwise. + */ + bool GetNextInterval(const SMILInterval* aPrevInterval, + const SMILInterval* aReplacedInterval, + const SMILInstanceTime* aFixedBeginTime, + SMILInterval& aResult) const; + SMILInstanceTime* GetNextGreater(const InstanceTimeList& aList, + const SMILTimeValue& aBase, + int32_t& aPosition) const; + SMILInstanceTime* GetNextGreaterOrEqual(const InstanceTimeList& aList, + const SMILTimeValue& aBase, + int32_t& aPosition) const; + SMILTimeValue CalcActiveEnd(const SMILTimeValue& aBegin, + const SMILTimeValue& aEnd) const; + SMILTimeValue GetRepeatDuration() const; + SMILTimeValue ApplyMinAndMax(const SMILTimeValue& aDuration) const; + SMILTime ActiveTimeToSimpleTime(SMILTime aActiveTime, + uint32_t& aRepeatIteration); + SMILInstanceTime* CheckForEarlyEnd(const SMILTimeValue& aContainerTime) const; + void UpdateCurrentInterval(bool aForceChangeNotice = false); + void SampleSimpleTime(SMILTime aActiveTime); + void SampleFillValue(); + nsresult AddInstanceTimeFromCurrentTime(SMILTime aCurrentTime, + double aOffsetSeconds, bool aIsBegin); + void RegisterMilestone(); + bool GetNextMilestone(SMILMilestone& aNextMilestone) const; + + // Notification methods. Note that these notifications can result in nested + // calls to this same object. Therefore, + // (i) we should not perform notification until this object is in + // a consistent state to receive callbacks, and + // (ii) after calling these methods we must assume that the state of the + // element may have changed. + void NotifyNewInterval(); + void NotifyChangedInterval(SMILInterval* aInterval, bool aBeginObjectChanged, + bool aEndObjectChanged); + + void FireTimeEventAsync(EventMessage aMsg, int32_t aDetail); + const SMILInstanceTime* GetEffectiveBeginInstance() const; + const SMILInterval* GetPreviousInterval() const; + bool HasPlayed() const { return !mOldIntervals.IsEmpty(); } + bool HasClientInFillRange() const; + bool EndHasEventConditions() const; + bool AreEndTimesDependentOn(const SMILInstanceTime* aBase) const; + + // Reset the current interval by first passing ownership to a temporary + // variable so that if Unlink() results in us receiving a callback, + // mCurrentInterval will be nullptr and we will be in a consistent state. + void ResetCurrentInterval() { + if (mCurrentInterval) { + // Transfer ownership to temp var. (This sets mCurrentInterval to null.) + auto interval = std::move(mCurrentInterval); + interval->Unlink(); + } + } + + // + // Members + // + mozilla::dom::SVGAnimationElement* mAnimationElement; // [weak] won't outlive + // owner + TimeValueSpecList mBeginSpecs; // [strong] + TimeValueSpecList mEndSpecs; // [strong] + + SMILTimeValue mSimpleDur; + + SMILRepeatCount mRepeatCount; + SMILTimeValue mRepeatDur; + + SMILTimeValue mMin; + SMILTimeValue mMax; + + enum SMILFillMode : uint8_t { FILL_REMOVE, FILL_FREEZE }; + SMILFillMode mFillMode; + static const nsAttrValue::EnumTable sFillModeTable[]; + + enum SMILRestartMode : uint8_t { + RESTART_ALWAYS, + RESTART_WHENNOTACTIVE, + RESTART_NEVER + }; + SMILRestartMode mRestartMode; + static const nsAttrValue::EnumTable sRestartModeTable[]; + + InstanceTimeList mBeginInstances; + InstanceTimeList mEndInstances; + uint32_t mInstanceSerialIndex; + + SMILAnimationFunction* mClient; + UniquePtr<SMILInterval> mCurrentInterval; + IntervalList mOldIntervals; + uint32_t mCurrentRepeatIteration; + SMILMilestone mPrevRegisteredMilestone; + static const SMILMilestone sMaxMilestone; + static const uint8_t sMaxNumIntervals; + static const uint8_t sMaxNumInstanceTimes; + + // Set of dependent time value specs to be notified when establishing a new + // current interval. Change notifications and delete notifications are handled + // by the interval. + // + // [weak] The SMILTimeValueSpec objects register themselves and unregister + // on destruction. Likewise, we notify them when we are destroyed. + TimeValueSpecHashSet mTimeDependents; + + /** + * The state of the element in its life-cycle. These states are based on the + * element life-cycle described in SMILANIM 3.6.8 + */ + enum SMILElementState { + STATE_STARTUP, + STATE_WAITING, + STATE_ACTIVE, + STATE_POSTACTIVE + }; + SMILElementState mElementState; + + enum SMILSeekState { + SEEK_NOT_SEEKING, + SEEK_FORWARD_FROM_ACTIVE, + SEEK_FORWARD_FROM_INACTIVE, + SEEK_BACKWARD_FROM_ACTIVE, + SEEK_BACKWARD_FROM_INACTIVE + }; + SMILSeekState mSeekState; + + // Used to batch updates to the timing model + class AutoIntervalUpdateBatcher; + bool mDeferIntervalUpdates; + bool mDoDeferredUpdate; // Set if an update to the current interval was + // requested while mDeferIntervalUpdates was set + bool mIsDisabled; + + // Stack-based helper class to call UpdateCurrentInterval when it is destroyed + class AutoIntervalUpdater; + + // Recursion depth checking + uint8_t mDeleteCount; + uint8_t mUpdateIntervalRecursionDepth; + static const uint8_t sMaxUpdateIntervalRecursionDepth; +}; + +inline void ImplCycleCollectionUnlink(SMILTimedElement& aField) { + aField.Unlink(); +} + +inline void ImplCycleCollectionTraverse( + nsCycleCollectionTraversalCallback& aCallback, SMILTimedElement& aField, + const char* aName, uint32_t aFlags = 0) { + aField.Traverse(&aCallback); +} + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTIMEDELEMENT_H_ diff --git a/dom/smil/SMILType.h b/dom/smil/SMILType.h new file mode 100644 index 0000000000..5aef1e7623 --- /dev/null +++ b/dom/smil/SMILType.h @@ -0,0 +1,212 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTYPE_H_ +#define DOM_SMIL_SMILTYPE_H_ + +#include "mozilla/Attributes.h" +#include "nscore.h" + +namespace mozilla { + +class SMILValue; + +////////////////////////////////////////////////////////////////////////////// +// SMILType: Interface for defining the basic operations needed for animating +// a particular kind of data (e.g. lengths, colors, transformation matrices). +// +// This interface is never used directly but always through a SMILValue that +// bundles together a pointer to a concrete implementation of this interface and +// the data upon which it should operate. +// +// We keep the data and type separate rather than just providing different +// subclasses of SMILValue. This is so that sizeof(SMILValue) is the same +// for all value types, allowing us to have a type-agnostic nsTArray of +// SMILValue objects (actual objects, not pointers). It also allows most +// SMILValues (except those that need to allocate extra memory for their +// data) to be allocated on the stack and directly assigned to one another +// provided performance benefits for the animation code. +// +// Note that different types have different capabilities. Roughly speaking there +// are probably three main types: +// +// +---------------------+---------------+-------------+------------------+ +// | CATEGORY: | DISCRETE | LINEAR | ADDITIVE | +// +---------------------+---------------+-------------+------------------+ +// | Example: | strings, | path data? | lengths, | +// | | color k/words?| | RGB color values | +// | | | | | +// | -- Assign? | X | X | X | +// | -- Add? | - | X? | X | +// | -- SandwichAdd? | - | -? | X | +// | -- ComputeDistance? | - | - | X? | +// | -- Interpolate? | - | X | X | +// +---------------------+---------------+-------------+------------------+ +// + +class SMILType { + /** + * Only give the SMILValue class access to this interface. + */ + friend class SMILValue; + + protected: + /** + * Initialises aValue and sets it to some identity value such that adding + * aValue to another value of the same type has no effect. + * + * @pre aValue.IsNull() + * @post aValue.mType == this + */ + virtual void Init(SMILValue& aValue) const = 0; + + /** + * Destroys any data associated with a value of this type. + * + * @pre aValue.mType == this + * @post aValue.IsNull() + */ + virtual void Destroy(SMILValue& aValue) const = 0; + + /** + * Assign this object the value of another. Think of this as the assignment + * operator. + * + * @param aDest The left-hand side of the assignment. + * @param aSrc The right-hand side of the assignment. + * @return NS_OK on success, an error code on failure such as when the + * underlying type of the specified object differs. + * + * @pre aDest.mType == aSrc.mType == this + */ + virtual nsresult Assign(SMILValue& aDest, const SMILValue& aSrc) const = 0; + + /** + * Test two SMILValue objects (of this SMILType) for equality. + * + * A return value of true represents a guarantee that aLeft and aRight are + * equal. (That is, they would behave identically if passed to the methods + * Add, SandwichAdd, ComputeDistance, and Interpolate). + * + * A return value of false simply indicates that we make no guarantee + * about equality. + * + * NOTE: It's perfectly legal for implementations of this method to return + * false in all cases. However, smarter implementations will make this + * method more useful for optimization. + * + * @param aLeft The left-hand side of the equality check. + * @param aRight The right-hand side of the equality check. + * @return true if we're sure the values are equal, false otherwise. + * + * @pre aDest.mType == aSrc.mType == this + */ + virtual bool IsEqual(const SMILValue& aLeft, + const SMILValue& aRight) const = 0; + + /** + * Adds two values. + * + * The count parameter facilitates repetition. + * + * By equation, + * + * aDest += aValueToAdd * aCount + * + * Therefore, if aCount == 0, aDest will be unaltered. + * + * This method will fail if this data type is not additive or the value was + * not specified using an additive syntax. + * + * See SVG 1.1, section 19.2.5. In particular, + * + * "If a given attribute or property can take values of keywords (which are + * not additive) or numeric values (which are additive), then additive + * animations are possible if the subsequent animation uses a numeric value + * even if the base animation uses a keyword value; however, if the + * subsequent animation uses a keyword value, additive animation is not + * possible." + * + * If this method fails (e.g. because the data type is not additive), aDest + * will be unaltered. + * + * @param aDest The value to add to. + * @param aValueToAdd The value to add. + * @param aCount The number of times to add aValueToAdd. + * @return NS_OK on success, an error code on failure. + * + * @pre aValueToAdd.mType == aDest.mType == this + */ + virtual nsresult Add(SMILValue& aDest, const SMILValue& aValueToAdd, + uint32_t aCount) const = 0; + + /** + * Adds aValueToAdd to the underlying value in the animation sandwich, aDest. + * + * For most types this operation is identical to a regular Add() but for some + * types (notably <animateTransform>) the operation differs. For + * <animateTransform> Add() corresponds to simply adding together the + * transform parameters and is used when calculating cumulative values or + * by-animation values. On the other hand SandwichAdd() is used when adding to + * the underlying value and requires matrix post-multiplication. (This + * distinction is most clearly indicated by the SVGT1.2 test suite. It is not + * obvious within the SMIL specifications.) + * + * @param aDest The value to add to. + * @param aValueToAdd The value to add. + * @return NS_OK on success, an error code on failure. + * + * @pre aValueToAdd.mType == aDest.mType == this + */ + virtual nsresult SandwichAdd(SMILValue& aDest, + const SMILValue& aValueToAdd) const { + return Add(aDest, aValueToAdd, 1); + } + + /** + * Calculates the 'distance' between two values. This is the distance used in + * paced interpolation. + * + * @param aFrom The start of the interval for which the distance should + * be calculated. + * @param aTo The end of the interval for which the distance should be + * calculated. + * @param aDistance The result of the calculation. + * @return NS_OK on success, or an appropriate error code if there is no + * notion of distance for the underlying data type or the distance + * could not be calculated. + * + * @pre aFrom.mType == aTo.mType == this + */ + virtual nsresult ComputeDistance(const SMILValue& aFrom, const SMILValue& aTo, + double& aDistance) const = 0; + + /** + * Calculates an interpolated value between two values using the specified + * proportion. + * + * @param aStartVal The value defining the start of the interval of + * interpolation. + * @param aEndVal The value defining the end of the interval of + * interpolation. + * @param aUnitDistance A number between 0.0 and 1.0 (inclusive) defining + * the distance of the interpolated value in the + * interval. + * @param aResult The interpolated value. + * @return NS_OK on success, NS_ERROR_FAILURE if this data type cannot be + * interpolated or NS_ERROR_OUT_OF_MEMORY if insufficient memory was + * available for storing the result. + * + * @pre aStartVal.mType == aEndVal.mType == aResult.mType == this + */ + virtual nsresult Interpolate(const SMILValue& aStartVal, + const SMILValue& aEndVal, double aUnitDistance, + SMILValue& aResult) const = 0; +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTYPE_H_ diff --git a/dom/smil/SMILTypes.h b/dom/smil/SMILTypes.h new file mode 100644 index 0000000000..6391872f9e --- /dev/null +++ b/dom/smil/SMILTypes.h @@ -0,0 +1,30 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILTYPES_H_ +#define DOM_SMIL_SMILTYPES_H_ + +#include <stdint.h> + +namespace mozilla { + +// A timestamp in milliseconds +// +// A time may represent: +// +// simple time -- offset within the simple duration +// active time -- offset within the active duration +// document time -- offset since the document begin +// wallclock time -- "real" time -- offset since the epoch +// +// For an overview of how this class is related to other SMIL time classes see +// the documentation in SMILTimeValue.h +// +using SMILTime = int64_t; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILTYPES_H_ diff --git a/dom/smil/SMILValue.cpp b/dom/smil/SMILValue.cpp new file mode 100644 index 0000000000..4942e3b8b6 --- /dev/null +++ b/dom/smil/SMILValue.cpp @@ -0,0 +1,142 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "SMILValue.h" + +#include "nsDebug.h" +#include <string.h> + +namespace mozilla { + +//---------------------------------------------------------------------- +// Public methods + +SMILValue::SMILValue(const SMILType* aType) : mType(SMILNullType::Singleton()) { + mU.mBool = false; + if (!aType) { + NS_ERROR("Trying to construct SMILValue with null mType pointer"); + return; + } + + InitAndCheckPostcondition(aType); +} + +SMILValue::SMILValue(const SMILValue& aVal) : mType(SMILNullType::Singleton()) { + InitAndCheckPostcondition(aVal.mType); + mType->Assign(*this, aVal); +} + +const SMILValue& SMILValue::operator=(const SMILValue& aVal) { + if (&aVal == this) return *this; + + if (mType != aVal.mType) { + DestroyAndReinit(aVal.mType); + } + + mType->Assign(*this, aVal); + + return *this; +} + +// Move constructor / reassignment operator: +SMILValue::SMILValue(SMILValue&& aVal) noexcept + : mU(aVal.mU), // Copying union is only OK because we clear aVal.mType + // below. + mType(aVal.mType) { + // Leave aVal with a null type, so that it's safely destructible (and won't + // mess with anything referenced by its union, which we've copied). + aVal.mType = SMILNullType::Singleton(); +} + +SMILValue& SMILValue::operator=(SMILValue&& aVal) noexcept { + if (!IsNull()) { + // Clean up any data we're currently tracking. + DestroyAndCheckPostcondition(); + } + + // Copy the union (which could include a pointer to external memory) & mType: + mU = aVal.mU; + mType = aVal.mType; + + // Leave aVal with a null type, so that it's safely destructible (and won't + // mess with anything referenced by its union, which we've now copied). + aVal.mType = SMILNullType::Singleton(); + + return *this; +} + +bool SMILValue::operator==(const SMILValue& aVal) const { + if (&aVal == this) return true; + + return mType == aVal.mType && mType->IsEqual(*this, aVal); +} + +nsresult SMILValue::Add(const SMILValue& aValueToAdd, uint32_t aCount) { + if (aValueToAdd.mType != mType) { + NS_ERROR("Trying to add incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->Add(*this, aValueToAdd, aCount); +} + +nsresult SMILValue::SandwichAdd(const SMILValue& aValueToAdd) { + if (aValueToAdd.mType != mType) { + NS_ERROR("Trying to add incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->SandwichAdd(*this, aValueToAdd); +} + +nsresult SMILValue::ComputeDistance(const SMILValue& aTo, + double& aDistance) const { + if (aTo.mType != mType) { + NS_ERROR("Trying to calculate distance between incompatible types"); + return NS_ERROR_FAILURE; + } + + return mType->ComputeDistance(*this, aTo, aDistance); +} + +nsresult SMILValue::Interpolate(const SMILValue& aEndVal, double aUnitDistance, + SMILValue& aResult) const { + if (aEndVal.mType != mType) { + NS_ERROR("Trying to interpolate between incompatible types"); + return NS_ERROR_FAILURE; + } + + if (aResult.mType != mType) { + // Outparam has wrong type + aResult.DestroyAndReinit(mType); + } + + return mType->Interpolate(*this, aEndVal, aUnitDistance, aResult); +} + +//---------------------------------------------------------------------- +// Helper methods + +// Wrappers for SMILType::Init & ::Destroy that verify their postconditions +void SMILValue::InitAndCheckPostcondition(const SMILType* aNewType) { + aNewType->Init(*this); + MOZ_ASSERT(mType == aNewType, + "Post-condition of Init failed. SMILValue is invalid"); +} + +void SMILValue::DestroyAndCheckPostcondition() { + mType->Destroy(*this); + MOZ_ASSERT(IsNull(), + "Post-condition of Destroy failed. " + "SMILValue not null after destroying"); +} + +void SMILValue::DestroyAndReinit(const SMILType* aNewType) { + DestroyAndCheckPostcondition(); + InitAndCheckPostcondition(aNewType); +} + +} // namespace mozilla diff --git a/dom/smil/SMILValue.h b/dom/smil/SMILValue.h new file mode 100644 index 0000000000..d57daa5eb3 --- /dev/null +++ b/dom/smil/SMILValue.h @@ -0,0 +1,75 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_SMILVALUE_H_ +#define DOM_SMIL_SMILVALUE_H_ + +#include "mozilla/SMILNullType.h" +#include "mozilla/SMILType.h" + +namespace mozilla { + +/** + * Although objects of this type are generally only created on the stack and + * only exist during the taking of a new time sample, that's not always the + * case. The SMILValue objects obtained from attributes' base values are + * cached so that the SMIL engine can make certain optimizations during a + * sample if the base value has not changed since the last sample (potentially + * avoiding recomposing). These SMILValue objects typically live much longer + * than a single sample. + */ +class SMILValue { + public: + SMILValue() : mU(), mType(SMILNullType::Singleton()) {} + explicit SMILValue(const SMILType* aType); + SMILValue(const SMILValue& aVal); + + ~SMILValue() { mType->Destroy(*this); } + + const SMILValue& operator=(const SMILValue& aVal); + + // Move constructor / reassignment operator: + SMILValue(SMILValue&& aVal) noexcept; + SMILValue& operator=(SMILValue&& aVal) noexcept; + + // Equality operators. These are allowed to be conservative (return false + // more than you'd expect) - see comment above SMILType::IsEqual. + bool operator==(const SMILValue& aVal) const; + bool operator!=(const SMILValue& aVal) const { return !(*this == aVal); } + + bool IsNull() const { return (mType == SMILNullType::Singleton()); } + + nsresult Add(const SMILValue& aValueToAdd, uint32_t aCount = 1); + nsresult SandwichAdd(const SMILValue& aValueToAdd); + nsresult ComputeDistance(const SMILValue& aTo, double& aDistance) const; + nsresult Interpolate(const SMILValue& aEndVal, double aUnitDistance, + SMILValue& aResult) const; + + union { + bool mBool; + uint64_t mUint; + int64_t mInt; + double mDouble; + struct { + float mAngle; + uint16_t mUnit; + uint16_t mOrientType; + } mOrient; + int32_t mIntPair[2]; + float mNumberPair[2]; + void* mPtr; + } mU; + const SMILType* mType; + + protected: + void InitAndCheckPostcondition(const SMILType* aNewType); + void DestroyAndCheckPostcondition(); + void DestroyAndReinit(const SMILType* aNewType); +}; + +} // namespace mozilla + +#endif // DOM_SMIL_SMILVALUE_H_ diff --git a/dom/smil/TimeEvent.cpp b/dom/smil/TimeEvent.cpp new file mode 100644 index 0000000000..b433709205 --- /dev/null +++ b/dom/smil/TimeEvent.cpp @@ -0,0 +1,58 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#include "mozilla/ContentEvents.h" +#include "mozilla/dom/TimeEvent.h" +#include "nsIDocShell.h" +#include "nsIInterfaceRequestorUtils.h" +#include "nsPresContext.h" +#include "nsGlobalWindow.h" + +namespace mozilla::dom { + +TimeEvent::TimeEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent) + : Event(aOwner, aPresContext, + aEvent ? aEvent : new InternalSMILTimeEvent(false, eVoidEvent)), + mDetail(mEvent->AsSMILTimeEvent()->mDetail) { + if (aEvent) { + mEventIsInternal = false; + } else { + mEventIsInternal = true; + } + + if (mPresContext) { + nsCOMPtr<nsIDocShell> docShell = mPresContext->GetDocShell(); + if (docShell) { + mView = docShell->GetWindow(); + } + } +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(TimeEvent, Event, mView) + +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(TimeEvent, Event) + +void TimeEvent::InitTimeEvent(const nsAString& aType, + nsGlobalWindowInner* aView, int32_t aDetail) { + NS_ENSURE_TRUE_VOID(!mEvent->mFlags.mIsBeingDispatched); + + Event::InitEvent(aType, false /*doesn't bubble*/, false /*can't cancel*/); + mDetail = aDetail; + mView = aView ? aView->GetOuterWindow() : nullptr; +} + +} // namespace mozilla::dom + +using namespace mozilla; +using namespace mozilla::dom; + +already_AddRefed<TimeEvent> NS_NewDOMTimeEvent(EventTarget* aOwner, + nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent) { + RefPtr<TimeEvent> it = new TimeEvent(aOwner, aPresContext, aEvent); + return it.forget(); +} diff --git a/dom/smil/TimeEvent.h b/dom/smil/TimeEvent.h new file mode 100644 index 0000000000..3b82e76378 --- /dev/null +++ b/dom/smil/TimeEvent.h @@ -0,0 +1,61 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#ifndef DOM_SMIL_TIMEEVENT_H_ +#define DOM_SMIL_TIMEEVENT_H_ + +#include "nsDocShell.h" +#include "mozilla/dom/Event.h" +#include "mozilla/dom/TimeEventBinding.h" +#include "mozilla/dom/Nullable.h" +#include "mozilla/dom/WindowProxyHolder.h" + +class nsGlobalWindowInner; + +namespace mozilla::dom { + +class TimeEvent final : public Event { + public: + TimeEvent(EventTarget* aOwner, nsPresContext* aPresContext, + InternalSMILTimeEvent* aEvent); + + // nsISupports interface: + NS_DECL_ISUPPORTS_INHERITED + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TimeEvent, Event) + + JSObject* WrapObjectInternal(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) override { + return TimeEvent_Binding::Wrap(aCx, this, aGivenProto); + } + + void InitTimeEvent(const nsAString& aType, nsGlobalWindowInner* aView, + int32_t aDetail); + + int32_t Detail() const { return mDetail; } + + Nullable<WindowProxyHolder> GetView() const { + if (!mView) { + return nullptr; + } + return WindowProxyHolder(mView->GetBrowsingContext()); + } + + TimeEvent* AsTimeEvent() final { return this; } + + private: + ~TimeEvent() = default; + + nsCOMPtr<nsPIDOMWindowOuter> mView; + int32_t mDetail; +}; + +} // namespace mozilla::dom + +already_AddRefed<mozilla::dom::TimeEvent> NS_NewDOMTimeEvent( + mozilla::dom::EventTarget* aOwner, nsPresContext* aPresContext, + mozilla::InternalSMILTimeEvent* aEvent); + +#endif // DOM_SMIL_TIMEEVENT_H_ diff --git a/dom/smil/crashtests/1010681-1.svg b/dom/smil/crashtests/1010681-1.svg new file mode 100644 index 0000000000..882bcb53fe --- /dev/null +++ b/dom/smil/crashtests/1010681-1.svg @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> +<script> +<![CDATA[ + +function boom() +{ + var animate = + document.createElementNS("http://www.w3.org/2000/svg", "animate"); + animate.setAttribute("dur", "2s"); + document.documentElement.appendChild(animate); + animate.targetElement; + animate.requiredExtensions.insertItemBefore(0, 0); + document.documentElement.setCurrentTime(4); + document.documentElement.setCurrentTime(0); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +]]> +</script></svg> diff --git a/dom/smil/crashtests/1322770-1.svg b/dom/smil/crashtests/1322770-1.svg new file mode 100644 index 0000000000..405435184e --- /dev/null +++ b/dom/smil/crashtests/1322770-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <animateMotion keyTimes='.1;.6;.6' path='m0,1l7,3' keyPoints='.6;1;.7'/> +</svg> diff --git a/dom/smil/crashtests/1322849-1.svg b/dom/smil/crashtests/1322849-1.svg new file mode 100644 index 0000000000..ea3d629813 --- /dev/null +++ b/dom/smil/crashtests/1322849-1.svg @@ -0,0 +1,2 @@ +<svg> +<set fill='freeze' dur='8' repeatCount='1844674737095516'> diff --git a/dom/smil/crashtests/1343357-1.html b/dom/smil/crashtests/1343357-1.html new file mode 100644 index 0000000000..8219c31221 --- /dev/null +++ b/dom/smil/crashtests/1343357-1.html @@ -0,0 +1,12 @@ +<svg> + <animateMotion to="500,500"></animateMotion> + <animateMotion to="10,40"></animateMotion> +</svg> +<svg width="100" height="100"> + <rect width="100%" height="100%" /> + <circle r="2" fill="red"> + <animateMotion dur="1s" from="50,50" to="80,70" additive="sum"></animateMotion> + <animateMotion dur="1s" from="50,50" to="80,70" additive="sum"></animateMotion> + <animateMotion dur="3s" to="0,80"></animateMotion> + </circle> +</svg> diff --git a/dom/smil/crashtests/1375596-1.svg b/dom/smil/crashtests/1375596-1.svg new file mode 100644 index 0000000000..69c1673d11 --- /dev/null +++ b/dom/smil/crashtests/1375596-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="800" height="600"> +<animate by="2" min="5:45" calcMode="discrete" attributeName="height" /> +</svg> diff --git a/dom/smil/crashtests/1402547-1.html b/dom/smil/crashtests/1402547-1.html new file mode 100644 index 0000000000..28fa7ce185 --- /dev/null +++ b/dom/smil/crashtests/1402547-1.html @@ -0,0 +1,3 @@ +<svg> +<animate id='a' calcMode='discrete' attributeName='height' by='-159'/> +<animate id='a' calcMode='discrete' attributeName='height' by='-159'/> diff --git a/dom/smil/crashtests/1411963-1.html b/dom/smil/crashtests/1411963-1.html new file mode 100644 index 0000000000..cc61b73e2a --- /dev/null +++ b/dom/smil/crashtests/1411963-1.html @@ -0,0 +1,10 @@ +<html> + <head> + <script> + const o1 = document.createElement('div'); + document.querySelector('script').appendChild(o1); + document.writeln("<svg><animate to attributeName='width'>"); + o1.innerHTML = "<meta http-equiv='Content-Security-Policy' content=default-src>"; + </script> + </head> +</html> diff --git a/dom/smil/crashtests/1413319-1.html b/dom/smil/crashtests/1413319-1.html new file mode 100644 index 0000000000..9bfeef3bdd --- /dev/null +++ b/dom/smil/crashtests/1413319-1.html @@ -0,0 +1,2 @@ +<svg width=''> +<animate dur='2ms' repeatCount='4611686018427387903' fill='freeze'/> diff --git a/dom/smil/crashtests/1535388-1.html b/dom/smil/crashtests/1535388-1.html new file mode 100644 index 0000000000..cdfaba4d90 --- /dev/null +++ b/dom/smil/crashtests/1535388-1.html @@ -0,0 +1,18 @@ +<html> +<head> +<script> +function start () { + document.location.assign('abc') + SpecialPowers.forceGC() + SpecialPowers.forceCC() + const XHR = new XMLHttpRequest() + XHR.open('GET', 'data:text/html,1', false) + XHR.send() +} +window.addEventListener('load', start) +</script> +</head> +<body> +<svg></svg> +</body> +</html> diff --git a/dom/smil/crashtests/1780800-1.html b/dom/smil/crashtests/1780800-1.html new file mode 100644 index 0000000000..14129a627c --- /dev/null +++ b/dom/smil/crashtests/1780800-1.html @@ -0,0 +1,22 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> +document.addEventListener("DOMContentLoaded", () => { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + const set = document.createElementNS("http://www.w3.org/2000/svg", "set"); + svg.appendChild(set); + document.documentElement.appendChild(svg); + const animation = new Animation(); + animation.addEventListener("finish", () => + svg.setAttribute("pointer-events", "visible") + ); + animation.startTime = 2713; + document.addEventListener("DOMAttrModified", e => { + e.originalTarget.setCurrentTime(1.050520798894502e38); + document.documentElement.removeAttribute("class"); + }); +}); +</script> +</head> +</html> diff --git a/dom/smil/crashtests/483584-1.svg b/dom/smil/crashtests/483584-1.svg new file mode 100644 index 0000000000..b9ded113ef --- /dev/null +++ b/dom/smil/crashtests/483584-1.svg @@ -0,0 +1,8 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <g id="s"> + <circle/> + <animateTransform attributeName="transform"/> + </g> + <use xlink:href="#s"/> +</svg> diff --git a/dom/smil/crashtests/483584-2.svg b/dom/smil/crashtests/483584-2.svg new file mode 100644 index 0000000000..f5cbd7d466 --- /dev/null +++ b/dom/smil/crashtests/483584-2.svg @@ -0,0 +1,133 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- =====================================================================--> +<!-- animate-elem-30-t.svg --> +<!-- --> +<!-- Tests various types of animations on referenced elements. --> +<!-- --> +<!-- Author : Ola Andersson, 22-Sep-2003 --> +<!--======================================================================--> +<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd"> +<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" baseProfile="tiny" id="svg-root" width="480" height="360" viewBox="0 0 480 360" onload="go()" class="reftest-wait"> + <script> + function go() { + var svg = document.documentElement; + svg.pauseAnimations(); + + // Note: Animations in this testcase have begin="100" dur="3". + + // Jump to partway through animation... + svg.setCurrentTime(102); + + // ...and then (if we didn't hang) jump back to a pre-animation time. + svg.setCurrentTime(50); + + // Signal that the test is complete: + svg.removeAttribute("class"); + } + </script> + <g transform="translate(20) scale(1.3)"> + <!-- SILHOUETTES--> + <path d="M210 40 C210 40 210 100 170 190" fill="none" stroke="#b4b4b4"/> + <path d="M 171 188 l 10 -10 l -10 -4 z" fill="#b4b4b4" stroke="none"/> + <polyline fill="none" stroke="#b4b4b4" stroke-width="2" points="200,20 200,40 220,40 220,60"/> + <polyline transform="rotate(15)" fill="none" stroke="#b4b4b4" stroke-width="9" points="200,120 200,140 220,140 220,160"/> + + <line x1="40" y1="50" x2="20" y2="10" stroke="#b4b4b4" stroke-width="3"/> + <line x1="160" y1="50" x2="80" y2="10" stroke="#b4b4b4" stroke-width="3"/> + <line x1="30" y1="30" x2="120" y2="30" stroke="#b4b4b4"/> + <path d="M 120 30 l -10 3 l 0 -6 z" fill="#b4b4b4" stroke="none"/> + + <line x1="70" y1="70" x2="145" y2="70" stroke="#b4b4b4"/> + <rect x="10" y="60" width="60" height="20" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/> + <rect x="145" y="60" width="30" height="40" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/> + <path d="M 145 70 l -10 3 l 0 -6 z" fill="#b4b4b4" stroke="none"/> + + <circle cx="30" cy="100" r="10" fill="#b4b4b4" stroke="#b4b4b4"/> + <circle cx="100" cy="195" r="15" fill="#b4b4b4" stroke="#b4b4b4"/> + <line x1="37" y1="107" x2="89" y2="184" stroke="#b4b4b4"/> + <path d="M 90 185 l -3 -12 l -6 5 z" fill="#b4b4b4" stroke="none"/> + + <rect x="300" y="10" width="20" height="20" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/> + <rect x="300" y="170" width="20" height="40" fill="#b4b4b4" stroke="#b4b4b4" stroke-width="2"/> + <line x1="310" y1="30" x2="310" y2="170" stroke="#b4b4b4"/> + <path d="M 310 170 l -3 -10 l 6 0 z" fill="#b4b4b4" stroke="none"/> + + <rect x="230" y="7.5" width="40" height="7.5" fill="#b4b4b4" stroke="none"/> + <rect x="230" y="170" width="40" height="30" fill="#b4b4b4" stroke="none"/> + <line x1="250" y1="10" x2="250" y2="170" stroke="#b4b4b4"/> + <path d="M 250 170 l -3 -10 l 6 0 z" fill="#b4b4b4" stroke="none"/> + <!-- END OF SILHOUETTES--> + + <!-- DEFS--> + <defs> + <line id="lineID" x1="30" y1="50" x2="10" y2="10" stroke="rgb(16, 93, 140)" stroke-width="3"> + <animate attributeName="x1" from="30" to="90" begin="100" dur="3" fill="freeze"/> + </line> + </defs> + + <defs> + <rect id="rectID" x="10" y="60" width="60" height="20" fill="blue" stroke="black" stroke-width="2"> + <animateColor attributeName="fill" from="white" to="rgb(16, 93, 140)" begin="100" dur="3" fill="freeze"/> + <animate attributeName="height" from="20" to="40" begin="100" dur="3" fill="freeze"/> + </rect> + </defs> + + <defs> + <circle id="circleID" cx="20" cy="100" r="10" fill="rgb(16, 93, 140)" stroke="black" transform=""> + <animate attributeName="cy" from="100" to="130" begin="100" dur="3" fill="freeze"/> + <animateTransform attributeName="transform" type="scale" from="1" to="1.5" additive="sum" begin="100" dur="3" fill="freeze"/> + </circle> + </defs> + + <defs> + <polyline id="polylineID" fill="none" stroke="rgb(16, 93, 140)" stroke-width="2" points="200,20 200,40 220,40 220,60"> + <animateMotion path="M 0 0 l 0 100" begin="100" dur="3" fill="freeze"/> + <animate attributeName="stroke-width" from="2" to="9" begin="100" dur="3" fill="freeze"/> + </polyline> + </defs> + + <defs> + <polygon id="polygonID" fill="green" stroke="black" points="240,20 240,40 260,40 260,20" stroke-width="2"> + <animate attributeName="fill" from="white" to="rgb(16, 93, 140)" begin="100" dur="3" fill="freeze"/> + </polygon> + </defs> + + <defs> + <image id="imageID" x="230" y="20" width="40" height="80" xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEASABIAAD/4RX+RXhpZgAASUkqAAgAAAAJAA8BAgAGAAAAegAAABABAgAXAAAAgAAAABIBAwABAAAAAQAAABoBBQABAAAAoAAAABsBBQABAAAAqAAAACgBAwABAAAAAgAAADIBAgAUAAAAsAAAABMCAwABAAAAAQAAAGmHBAABAAAAxAAAAGYFAABDYW5vbgBDYW5vbiBESUdJVEFMIElYVVMgMzAwAAAAAAAAAAAAALQAAAABAAAAtAAAAAEAAAAyMDAyOjAxOjE1IDA0OjQyOjU4ABsAmoIFAAEAAABWAwAAnYIFAAEAAABeAwAAAJAHAAQAAAAwMjEwA5ACABQAAAAOAgAABJACABQAAAAiAgAAAZEHAAQAAAABAgMAApEFAAEAAAA+AwAAAZIKAAEAAABGAwAAApIFAAEAAABOAwAABJIKAAEAAABmAwAABZIFAAEAAABuAwAABpIFAAEAAAB2AwAAB5IDAAEAAAAFAAAACZIDAAEAAAAAAAAACpIFAAEAAAB+AwAAfJIHAJoBAACGAwAAhpIHAAgBAAA2AgAAAKAHAAQAAAAwMTAwAaADAAEAAAABAAAAAqADAAEAAABABgAAA6ADAAEAAACwBAAABaAEAAEAAAAwBQAADqIFAAEAAAAgBQAAD6IFAAEAAAAoBQAAEKIDAAEAAAACAAAAF6IDAAEAAAACAAAAAKMHAAEAAAADAAAAAAAAADIwMDI6MDE6MTUgMDQ6NDI6NTgAMjAwMjowMToxNSAwNDo0Mjo1OAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAAAAAQAAAPUAAAAgAAAA1QAAACAAAAABAAAAyAAAAGQAAAAKAAAAAAAAAAMAAACs3QIAAAABAP//AADoAwAAMAEAACAAAAAMAAEAAwAmAAAAHAQAAAIAAwAEAAAAaAQAAAMAAwAEAAAAcAQAAAQAAwAaAAAAeAQAAAAAAwAGAAAArAQAAAAAAwAEAAAAuAQAAAYAAgAgAAAAwAQAAAcAAgAYAAAA4AQAAAgABAABAAAAcVYQAAkAAgAgAAAA+AQAABAABAABAAAAAAAEAQ0AAwAEAAAAGAUAAAAAAABMAAIAAAADAAEAAAAAAAQAAAABAAAAAAAAAAAAAAAAAAAAAwABAAEwAAD/////BgKtACAAdADVAP//AAAAAAAAAAAAAP//AABABkAGAgAwAdMAngAAAAAAAAAAADQAAACPAD8B1QD1AAAAAAAAAAEAAwAAAAAAAAAHMAAAAAAAAAAA//8AANUA+QAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAElNRzpESUdJVEFMIElYVVMgMzAwIEpQRUcAAAAAAAAARmlybXdhcmUgVmVyc2lvbiAxLjAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADAPQA9AD0AABqGADOAAAAgE8SAJsAAAAEAAEAAgAEAAAAUjk4AAIABwAEAAAAMDEwMAEQAwABAAAAQAYAAAIQAwABAAAAsAQAAAAAAAAGAAMBAwABAAAABgAAABoBBQABAAAAtAUAABsBBQABAAAAvAUAACgBAwABAAAAAgAAAAECBAABAAAA9AUAAAICBAABAAAA7g8AAAAAAAC0AAAAAQAAALQAAAABAAAA//////////////////////////////////////////////////9//////////////9j/2wCEAAkGBggGBQkIBwgKCQkLDRYPDQwMDRwTFRAWIR0jIiEcIB8kKTQsJCcxJx4fLT0tMTY3Ojo6Iio/RD44QjM3OTYBCQkJDAoMFAwMFA8KCgoPGhoKChoaTxoaGhoaT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT09PT//AABEIAHgAoAMBIQACEQEDEQH/xAGiAAABBQEBAQEBAQAAAAAAAAAAAQIDBAUGBwgJCgsQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+gEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoLEQACAQIEBAMEBwUEBAABAncAAQIDEQQFITEGEkFRB2FxEyIygQgUQpGhscEJIzNS8BVictEKFiQ04SXxFxgZGiYnKCkqNTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqCg4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2dri4+Tl5ufo6ery8/T19vf4+fr/2gAMAwEAAhEDEQA/AOxApcV7J5goUngDNIVweaBC80oYjvRYLseszAY60FjI3c1HLZ3L5r6DjG46jFRFjTVmJtoA5qW3j3vuIBAokuVNgnzOxpKVA5xUNyY3TB4Fc0b3Oh2sZbqAxx0pMV2pnG9xNtJincA20hWmBMBT1XJrJuxaLf7pU4wKqScuayp3vqaTtbQABjmnRJunQdiRVtuz8kQlsa0kUUg+ZFP4VVeyUNuiJB9DXn060oabx6o7Z0oy12l3I5pMDawwcVTIrupbX6PY46u9uq3ExT0kZBxWrV9DNOw4zue9MZi3U0lFIbm2NwKTFUIMUmKYgxSYoAmAFP2+9Z3NLCbaTbzRcmw9baR13KpIp8ETLcpuUjnvWcqsbNXXMk9DSNOV07aX3NInFNPTmvLO8iljWVcN19az2hZZNmMkniuzC1LXg9t0c2Ihe0lv1JZrGWEZIBX1FV8V1U6iqK6+aOacHB2fyYYorQkMUmKYgxSYoEGKTFAyYCnYrM0FCM33VJ+go8mQfwN+VS5xWjaTDlk9kzSRBHGq+gpoO6ZB6mvLk7tvuz0ErK3YlkIQVAz0hkZamhlWRXIztNCdtVuDV9zRDBlBGCCKz7uyO8NCuQeoHatqFTkld/C9zOrDnj5rYYunSn7xVfqaf/Zn/TTn/drpli4p6Lm89jnjhn1dvIo3TRWt5HbGQNNICwUdQB3NGK3pVPaR5tvIyqQ5HbfzDFJitDMMUYoAlxSgVmaGlAAkCjvjNKeTXlzd5N92ehFWSXZCHmoyQjBj2qGUMkn3GojJQA0yUKGf7ozQBbtd6x4fgZ4qxmhMGGcCsjxP4ktvDGiS39yQSPljjzy79hTEcN8P7y51mbUNd1KQGSZxGrHgDP8ACPyFdtivRw1vZ6dHqcNe/P6rQcIXPRGP4UxsLJ5bEB8Z298etbc8b2ur9jLkklezt3DFGKsRLQKzLNFJULiPeofGduecfSiR/wB4qD6mvIb/ADPSQuPWqt+3lxKfVgKT2AqmSk3E9KALUFkW+aTgelWtqhhGgx647Cj+kBI/3Pl7UwOCMijqBICGXNcj448Ax+MfLkN/NbyQqRGmA0fPcj+tMCPwz8PRpWnWcN/ePKYG83yovljL+p7nH4V1sjQWcLSyFIo0GWduAB9apSaVk7J7onlTd+q2Z5n4x+MKWxa08PKJW+610w4B/wBkd/rWr4Es7pdE/tDUpHlvb7EsjyHJ2/wj8j+tb4WN537IyxDtC3dnSYpMV6Jwkt8sktm6wSLFMynY5G7afpXkl3Y61qVyY7zVrkxkncN52kewBx+lediJuKWrSfQ76EVJvRO3Ut6zo0+p3Ed19rc3MUYRC3QADjGOhrofCGvz6bpPk61cyTXab9ryMWLDjA3f56VxKVzqcbGA/wAWdZtrly8CGLcdvm2xAx9Q2f0rc0P4lDxPKbae2t4XjXzN0UrENj/ZZRj86p/CQtzoZryKCJpZpFSNeSxPArk7z4iTC/UaMtkYUJ3SXT43n0AH86Fq7feGxq6Z8VbOWdLfVbY20zEKrwSCaMn8OR+VXbn4laFp9xLDJJcSzLkuIoSQvtk4FNiSuZU3xhtCT9i0i7nHrJIkf9TWx4Y8YxeIIpC8Is5g3+pMofH48UPa4Lsak3iTTbGyuJ7m6RI7Ztsp67T9PxqDTPG2ia7c/ZdMvBNMRu27COO55FF9ANHV9Zs9D097u+lEcSD8WPoBXhPjf4g6j4puWt4w9tYqflhBwW929aa19AMTw9aW93rlnDqEghtjMDIzdMelfREQR4lMJUxkfKV6Y9q7sK1Z9zkxKd12HbKPLrsucyRxsuuXeq25SOUKu8gPESpOPftWMblrWcLJueI/dmHI/E/1rw61R1Ja9NkevSgoR069TQVy6EoVbjiqsy3ZiO+GFXGNoEpIP47axNSgEvPNEF2trI0udqtKRkAc8bKbaaVGkpntbK383JV2W4ZcH0+7zVXttdE2vvZly4tLq8h8q5t4pIyeVN45H/oFVR4cjwB/Zdpjtm6f/wCJpKVurHy+SLFto2m7leK3iEqHBKMTtYe9OGmafcM8hgjkdnIdjzn1pOTGoortoMIkbytNsiueCzkE/wDjpp8FobWbZb2llE57JMQf/QafN5sXL5IfPo8l4jR3SoiH5iEkLZYdCQRin2tzc6FeR3S+XIkYwX+6Rn17VUZW03RMoX12Zj+JdQufEOprcXOr26LH/q4lOQtZc2keYd0uqQMQOpQZquZ9mRZdyGDRx5+WvbfAPH3Rmuy0XXLzw/A/2cG7gJ4QPuVfy71pTquDvYmcFNWbNmx+JFoqyfb4pyxclfLjXCr2H3ucetaVt4+0W4OPOkiP/TRMfrXbHFQe90zklhpLazRyMNxZ21v5K288aHPApIb3TbW0S2iSZUUbQCCePQ15soy6pps7ozj0asgtLnTbIsIXmVDzsIOF+lWDqliwO6duTkEjpUtd0ylLzRQ1KPS9TMZkvZImiyFaNtp5x/hViyfT9Ph8q3uiUzn5jk/nQ2rWBb3Lkd/bYybhT7U4X9sDj7SnHvU6FXZS8i3NvPHHqAjMzM+4EAqSe1Q6fNp+kWotZNTEzK2Q0jYNVvsTe2uyJW16yQEC+gbHvTtN8S6PaXsk1xHFdyOVKlwMIADkA571SjZ9/IHJNb2H3niawu7xpYpEgjzxGqggD65rOnbTjayQx3yqkyjOV3EHGOxH5UW12Fdd9jMtvDmhxXsdw140qoM+U0eVc+/tTNV8PWd/J5kV4kSseALfvgZ6DNac2mxHKu5WPgm23AW+qOwZQDut2Xn04zxnFT2ng9LEiSLWbm3nGdzRWz4Ht2zS5vIfL5mja2chB+33kF1nAz5Tow/HFOm0mFoSI7xAwPyoyEjHfnH9PWpv6jS80dK8cJGdwz9aiFvHnOBXqSVzzouwvkJnBVc/SnizQ/8ALMflXPKKNVJgbCIjmMflUf2C3brEpH+7kVm4ItSGtpdqRzBHj12CoG0mzbAMMeO3FR7MrmFGkWfCmEfhUM3hyykk3FXBPo5oVJBzsrt4QsHck+aPo/8A9aoz4K08E/vJP0P9Kr2fYnnI5PBViQQJZF/Af4VA3gi1B4nbp/cWj2TH7Qhk8GQAACZuneMCq0vhDYMpMufdf/r1SovuJ1UVz4WlLcNH9cH/ABqQeDLhjlZIPx3CmsPJ9SXWS6Cnwhfx/ceL8JmH9KP+Ea1ZD8s34i5Yf0p/Vp+TBYiPmd35eOgIoAJHBH511M50O29C4U47mnhQykBiT6BqxZqhwJTG4N9OtSrtY98+/Ws5eRcfMCgJzj2yKjaMZ64x6VCZTQ5VULyfxNIGRgR5gb2NUri0HeUj5GUcdhjpQzIBhlyvrkD+tUtfInReY37RbO2zcPTOajaKFXz5hI+uaaUkDcWRzRIVzge2BVaXYCBzitY/kZSsvmQmNQc5qSEAg7cH29K0iRIcSGGGcj2yKcANuA/Hck5NWQSzzKR8u/B6hRinIwAA+bH8qybNUSD/AH2A9MnFPDYXA5H51k0aIRkDMA0av6e1SCFduCij6VnJtFJJiGCI9ET64puVU4wc+oBqE2y7JApTdtXeD64OKcx6DJB9s1RIi5ZuCAMdQc0hWRELABiO3JNVdE2ZD+9UBinPcBj/AIUiXEj5DAoMd8mrsmTdr5jHBA+VcH1AzUTKyj69+laRsQ7kZXPynDe2KdHDGP8Alnj8K0RmxzBAOij0HSnxIsikMiqPUNVCHS28RxvJJ+tMEaRP8oOPUtmsWaIsIePf2pySgdT9NwrFo1TFNxs5YoR2waYssLO3mJHk9e+aVn0uO662BhCWyhUevz8flTiM/wCrOf8AgVRfv99i15fdcbmVcMUZiB0UgioJL64QkfZZAPUNVxSe1iJSa6MbHfSsQVtnXuS3FTfanYjfGoxxgfMavl8yObyHl02kLGcn/pkaYJJBn5MA+vGaSXdjv2RCbmYEqVXPs1NeUsvJXPrWkUlsQ5N76EQjycb2+meKsIDj1x6CtIoiV+o/eQvfHuKA/wAvYfhVkjC0gHVQfXFRkXDYO9GI6DFYNmthn+mZz8uP9lQamjmuVA/csffgVDsylddCdLhgp3oE79aie5lxhFWQexJqeW3Urm8vkVvt8gfbIoA/3CDVhJVxlCg+oNJx82ClforoZNdXce0pFGy/3gc5pq307H96kZB7KDQoIHN9kSf2gVOFXr2KkU7+1sAB1z9Oark8xe0EXU4n6RsD9KSd4WQFhz24zTUXHzE5KXkQOY9uMNjsNtIEj2/xfTbjNap+RnZdxu1edq7f6frT0hdlyJG/4C55ql9xLEMjxcBps/gc09LknO/cfZkH+NNaAOUTyjLsY19AaUzJHncWPrmsWro1TsC38J6tge4NONxBMAFc59FJrFprbU1Ti9HoD2sLJhmbHo3SmJZwA5Bj46dqqM7kygiVJkiB+YnPbPFMa9jfcFwSPek43dxqSSsNa7mUbvLVB7tSHUHWLcYmHtkHNCh5sXP5EB1p0TAtHx6g4/lU0OqQyLlrd93q4z+tVZ9G0LnXVCyXdw5/cBMf7uaga+uQwLxIT6gYqlHzZLl5KwNcs7ZljOT6UmYznkp+JFaIhjMRqM+ac/72aljDsR+/z9GqkTYk8uRTnex+rVIszIMfOSfamB//2apR82S5eSsDXLO2ZYzk+lJmM//Y/+0O2lBob3Rvc2hvcCAzLjAAOEJJTQPtClJlc29sdXRpb24AAAAAEABIAAAAAQACAEgAAAABAAI4QklNBA0YRlggR2xvYmFsIExpZ2h0aW5nIEFuZ2xlAAAAAAQAAAAeOEJJTQQZEkZYIEdsb2JhbCBBbHRpdHVkZQAAAAAEAAAAHjhCSU0D8wtQcmludCBGbGFncwAAAAkAAAAAAAAAAAEAOEJJTQQKDkNvcHlyaWdodCBGbGFnAAAAAAEAADhCSU0nEBRKYXBhbmVzZSBQcmludCBGbGFncwAAAAAKAAEAAAAAAAAAAjhCSU0D9RdDb2xvciBIYWxmdG9uZSBTZXR0aW5ncwAAAEgAL2ZmAAEAbGZmAAYAAAAAAAEAL2ZmAAEAoZmaAAYAAAAAAAEAMgAAAAEAWgAAAAYAAAAAAAEANQAAAAEALQAAAAYAAAAAAAE4QklNA/gXQ29sb3IgVHJhbnNmZXIgU2V0dGluZ3MAAABwAAD/////////////////////////////A+gAAAAA/////////////////////////////wPoAAAAAP////////////////////////////8D6AAAAAD/////////////////////////////A+gAADhCSU0ECAZHdWlkZXMAAAAAEAAAAAEAAAJAAAACQAAAAAA4QklNBB4NVVJMIG92ZXJyaWRlcwAAAAQAAAAAOEJJTQQaBlNsaWNlcwAAAAB3AAAABgAAAAAAAAAAAAAAWgAAAHgAAAALAG8AcABlAHIAYQBfAGgAbwB1AHMAZQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAeAAAAFoAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAOEJJTQQREUlDQyBVbnRhZ2dlZCBGbGFnAAAAAQEAOEJJTQQUF0xheWVyIElEIEdlbmVyYXRvciBCYXNlAAAABAAAAAE4QklNBAwVTmV3IFdpbmRvd3MgVGh1bWJuYWlsAAALLwAAAAEAAABwAAAAVAAAAVAAAG5AAAALEwAYAAH/2P/gABBKRklGAAECAQBIAEgAAP/uAA5BZG9iZQBkgAAAAAH/2wCEAAwICAgJCAwJCQwRCwoLERUPDAwPFRgTExUTExgRDAwMDAwMEQwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwBDQsLDQ4NEA4OEBQODg4UFA4ODg4UEQwMDAwMEREMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDP/AABEIAFQAcAMBIgACEQEDEQH/3QAEAAf/xAE/AAABBQEBAQEBAQAAAAAAAAADAAECBAUGBwgJCgsBAAEFAQEBAQEBAAAAAAAAAAEAAgMEBQYHCAkKCxAAAQQBAwIEAgUHBggFAwwzAQACEQMEIRIxBUFRYRMicYEyBhSRobFCIyQVUsFiMzRygtFDByWSU/Dh8WNzNRaisoMmRJNUZEXCo3Q2F9JV4mXys4TD03Xj80YnlKSFtJXE1OT0pbXF1eX1VmZ2hpamtsbW5vY3R1dnd4eXp7fH1+f3EQACAgECBAQDBAUGBwcGBTUBAAIRAyExEgRBUWFxIhMFMoGRFKGxQiPBUtHwMyRi4XKCkkNTFWNzNPElBhaisoMHJjXC0kSTVKMXZEVVNnRl4vKzhMPTdePzRpSkhbSVxNTk9KW1xdXl9VZmdoaWprbG1ub2JzdHV2d3h5ent8f/2gAMAwEAAhEDEQA/AOmAU2VPf9ETHJSDURr7GCG6BaJJ6NMeKJzdpjwTahTMkkn5pnTGunx0SvZHdKKcoMDw0lrhIjXRRrrdZu1gjkHlX2yK2DwaPyKFjQ/nQ/vDlVvvGpsDzDY9nQUT5FoPBa6FGXcSiW1uY4h2s8HxTPrex217S13gdFZiYkDY2wGwTvo2an1VMiQh5F1VjdsSeyBCUICAu7KuM1VMITQiEJoT7WMNqbapwlCNqf/Q6sBTZW95hgkjUplZxPovI5JA+5Xck+GJLVhHikAiZjWCxu8DbMnVWbX+09/xTmAQO5/ImcNCfAKpPIZ1daNiEBG66snlrWD4D8iC53igesXAGeQITbydBr5JlrkhcJDiNxaQ4D4K48V31gO9zHag9xPcKqzGfAdZpOjWjklHawVAMBkfx5KVnTwVQa4wXk+54A7dynODW1pc63Y1oLnvdDWtaBue97ifaxjfpK0NeFx3+Md/1it6fX03peHc/DySPtmXUN8ydteJtq3W11bvfc9zP0v80pffyfvfgFnsw7Ovi5VGZQ3KxiXY9hd6NhEb2NcWNu2/mtt272fyEWFHpODlMxG0WUfZKMcMpxhbHqvrYxodfbSwv9H1Ld/p1P8A0npfzv6RR631PovQMcX9UyjW5wmrHrAddZH+ipn/AMEs2VKwM8BEXLWtdGA4ZWaGl90kJQhYN1uVh05N1P2Z17fUFBO5zGO91LbXQz9N6Wx9vt9n82jwpQbAPdjqi//R6D6xX9Qx+n+r0p9NWRuaHeqN52nT9CyW/pGrnas3602YRZZ1U15DrRcLamBpDWtLPs3t/wAE/wDnLPalaMzJqpdl0323k7R7txpDnH9J6jj9Gpuz6KakZlVjab632jtkMaQyR+bcyT6Lv/An/mJZcspHSxFOPGIjWiXSwfrP1LG6Re/qbftnWatwoZWw7Lmy30f5oNbvb+k9T+bVKr/GcW2DG6p0v7K+0hm8Pe0N3e3dtsbY13P76Fdhm1hrc/IIJgu0a4T+axzam+z+V/4IqdJtN/2V1GW2qpz2G+xrHVbBP2d9T3U/ztm36DvoKMSOui4xHd62/qfT8PGORk5NdWPUIdYXAzHthjQdz3Pj2LGx/wDGNg02usdjUuxjGwjIAytv77qXsON7/wDRttVduIwmRZYSNTLav/edK2Mf03tNl++xtRa4MhrX+19x9Khrv0aQl4JMPF23/wCMD6tsZRkOdkuGRW6yqsVDeGjcLH2TZsb/ADbm/SVGz/Gf0h5Apw7yyRNll1LC3X6fpNdY93/FrPx8pt2TkUV1PaKNgNjmQHEn3Gt30XMYpWFja33C+6QC8MYWdtdjG+kXJcfgrg8Xrx1/o7Ayx+ZWGW1WXNM/mVAWW7o+g/Z9Gt300uk/WPpfV7jX019zrGNFjvUpfUdjtGmsv/nNzv3Fy13S3041Vue4ltx/mTZVc2RB22Nqr/N3LPtycnp4fZgMymWAij7NW4hz2Wbv0mM/32VtRBO1I4eoL0H1w+u/7GZZhdJr+09Qb7brtu6nGJ/NsI9luV/wX0Kv8KvOul+n1P6wY9vX8hxquuD8vJvl5cGje2l+jttdzw2r9yqtWG4+O3Edl2dKvrq3loabbd5MbnvcyPo7js9T8+z/AItV2ZWDW8XO6blBp9zQXOLYafcR6gc16cCQQaWEWKt9iIL/AHghweNwc2C0g67mub7dqbYV5vifWTIxqaqsa+7Bqa8PrxcmpzWF8+r7bqf55tn+Eru9Nlq1/wDnn1+gzk4+PY0Qfax7ZB/PZayx9StDmY9QQ1zy8uhBf//SC/pmKPoOIJ7Ne7+9MOnVNH85aB4ixytuDWHbMHwCeAAZPPYj+KsSxw/dH2NeM5dz9rXHTTGl14/tlI4FjSP1rI8hvn8FbDARMRroSf4Byk3cCQWkjsY5CiOOPYMgnLuWj9iuDdMy6PEkEf8AUqIx8sGRnWgdx7T/AN9WkS3ktOneFGAHTB1TeCPZdxy7uZkYXUrdhZ1S1gHGgjy42qpZ0jrTy6OruI7BzT+UPW+Tydp2nvIj/NTzXzHuHiATHyThCKDOXd5odE6wyNnUmeGrXAfDlMenfWMBv+UK3bRDY3cf5hXSOuLCP0YduMl2g/6MlRc2ppLgzbPYnUf2DuR9sI9yXd5l+P8AWVp3DMa4wRydPL3M9qGLPrQ1wAu3RwHODm/9L81dHZsk7WwSBogOaOQ2T3AMpwwx8VpzScYH63kfzm4Dgeo0/wDVOSFv1tYI2jaeRLII/lAXLcZu2CA4eIOhUgNCdr4/OiSnfd4eP4f96t+8T8H/05PZkB28OboPzi7/AL6p1OJEz8Cf9qC4vBEPY13jAJj71HZlfmPZH9Uz+VWZFrC240tPmR2EGfkkHMaQBo4nUOEx+IVXfcwAWmrXncQD9wCT8qn6IDNwGo2kg/cmEWuum4TYCI2OB8AZ/wCqc1DLniR7te7SQNP5YZ7VUD73O2iljmd/T0Mf5zXf9FGDrhpWAHRoHgx83Nc5NqlwNsn5uMyA+yHd9Hc+b9j0qs3FfoHbxoZa0kT/AJqGLuonSxrXDuGz/wBUXKX2jJA1phvYOg/IJwH8rWkn+QZzjuJdIYXcgkA6eA9Ru1QYamglu93jALv836X/AFSTb8h3/aQx5QP+qTWixxaHVWb+4B0H4ohHl+SxdU7hrpI5cDz/AGoQvdOjRH3H7lElu73bh81DfXJDXAHydJ/KpAsJbQIiCSPiRP4p2BocXAy7xMfwQCS0Ai+Y/N3NEfDcFJj73HVu8eHt/wC+7U+1r//UGz0tvs/nO+6N0/20x+3bvZt2x7d0fjuXl6SsS2aw3/g+qV/bJHq7Nvlz89ym7kT6cfPn+wvKElGN/wDvWQ7f98+rH7Rr6e3nSP8AzJB/WIHr7v8Arcx5bvzti8vSTvs/7pb9v7H039N6b9vq+XO3+xv/ADkAftLc3Z9q/wCjt+Xqe1ecpIlH2vp1vrbB9t3bZ1mN3/R/QquPT3D0/V2eUxH/AFK85STo/RbL6/V9Lb638rbP50Qmd6kGfS51mV5qknrX0+v0tv5v9nd/FPp+bO3vG3/0Z7l5ekih/9kAOEJJTQQhGlZlcnNpb24gY29tcGF0aWJpbGl0eSBpbmZvAAAAAFUAAAABAQAAAA8AQQBkAG8AYgBlACAAUABoAG8AdABvAHMAaABvAHAAAAATAEEAZABvAGIAZQAgAFAAaABvAHQAbwBzAGgAbwBwACAANgAuADAAAAABADhCSU0EBgxKUEVHIFF1YWxpdHkAAAAAB//+AAAAAQEA/+4ADkFkb2JlAGSAAAAAAf/bAIQAFBERGhIaKRgYKTMnICczJxwcHBwnIhcXFxcXIhEMDAwMDAwRDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAEVGhohHSEiGBgiFA4ODhQUDg4ODhQRDAwMDAwREQwMDAwMDBEMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM/8AAEQgAWgB4AwEiAAIRAQMRAf/dAAQACP/EARsAAAMBAQEBAQEBAQEAAAAAAAEAAgMEBQYHCAkKCwEBAQEBAQEBAQEBAQEAAAAAAAECAwQFBgcICQoLEAACAgEDAgMEBwYDAwYCATUBAAIRAyESMQRBUSITYXEygZGxQqEF0cEU8FIjcjNi4YLxQzSSorIV0lMkc8JjBoOT4vKjRFRkJTVFFiZ0NlVls4TD03Xj80aUpIW0lcTU5PSltcXV5fVWZnaGlqa2xtbm9hEAAgIABQEGBgEDAQMFAwYvAAERAiEDMUESUWFxgZEiEzLwobEEwdHh8UJSI2JyFJIzgkMkorI0U0Rjc8LSg5OjVOLyBRUlBhYmNWRFVTZ0ZbOEw9N14/NGlKSFtJXE1OT0pbXF1eX1VmZ2hv/aAAwDAQACEQMRAD8A9dIiTwoDYJi+o85BjSGibVoDGMjqFN3RdsOgbIvl5O8M6Kso5pRIYt2yRPycqd1cow8CoanV33gPMrWpCcFZCJcOdNIaiApFNUtNITSppUD/0PaSrpiGtvpbhScEpcGdF32gCmigvG1pOqrAwAAQSyZVoxucGiyWsVAEON3w6RgQbUgGTF3j9DIwyL0WrtXaRjijlnD04mUiAByw+L1f4kOt6uOCEqwYzuyS+zOWP/31ifcx/wA2InEGiLelbTqZtWNCVay7cETPJIRiO8izCQnESHBFu009DDTQqmlaQ//R9bqs8elh6kgSP8IeDH+MmUZShilYraJfbXP1ks8KiAI9/wDE82OYlpwfB1a+yJWu7PV6L8Tj1tjaYGHxCTcfxXpJGvUA/qEovjysWQDfjfxPBLpISu47SNfJJ5ybg+p9SMyZRIMTwQ55epxYK9WQjfD5GCWXBAQjZA/i2f8AbcJdNOUjMynuPe4KRB9Thy4px3YpCQ8Ync2ckMY88gP6jtfk49NGN5BM2QYk2xL8Ph2s1/T/AO/VIg+rx9RjykjHKMq/gludgbflMEJdLLdCwT7YNZo5swAlI7QTLbu/i/8AKmFSIPo/2TD6nqbI7/4trxfif4xi/Dxt+LKeIfw/+PfNH4hk6bp/Rx0JjieSXwx/9+PiT6eWQ7pGJJ5lu+JskOnpZZvxjqonMbjHzyj9iMY/7v8A8yP1z8b0ccvTz3wkIn2S+J9+P4vGNDLEg/bl/wDAnrSyRztVs9OleWH4n009BOv6gVevJdTnxZ//0sSCAImAoeBWRMzZhqOPM9BJPKu3RGFdmXqS7wP0uOSEMkt08crHte0Jq3PE1yOYZa+zJP7QB9mX0PTtZMXPEvI4JjHKO0idXu0QevgNdsg+iIrsbxHI82H4nCE9+3cRp5wjJ+JY8hJmDR+yH0vT91olgHgD8m8ScjxcuTps/wAYkaG0fv6TOWXTZQBUogaAD/40+ueniR8I+hyl00DzEfQ3iycjyPR6Q95gfv8A+UnaEsGPSE5iP8JO+P8A7rez9kx3rEN/smH+FvBk5o5Tl6aX2iFen9ixfw/eVXtsvNH/06J1oNA+DBjE9liaezOKNBomrLIl4JJLk0VXtQdGdDoQgHwclLGq34MkS7EfNz/mX2+9pDY252Y66tDepvwpoBd6uZtJmfH7mTO3SMsFW1TAIDYmPEOkZDEX3VIKtB//1J0lwLX04ns19l55c93qzkb+mBxakyHA+lxj/qeg/P5MGBnsMvY3t2jU2svm49+6YRcowJ5+9aA03UiXbhwyc9vmgdIG74ZLu2nWX3PHj57fJ6u3b/V8TQEzB+0iz73Lx4SP3pqMlX4hom+AQx37uw4/N0QmMfaqnnsqB//Z"> + <animate attributeName="y" from="5" to="145" begin="100" dur="3" fill="freeze"/> + </image> + </defs> + <!-- END OF DEFS--> + + <!-- ACTUAL TEST CONTENT--> + <use xlink:href="#lineID"> + <animate attributeName="x" from="10" to="70" begin="100" dur="3" fill="freeze"/> + </use> + + <use xlink:href="#rectID" transform=""> + <animateTransform attributeName="transform" type="translate" from="0 0" to="140 0" begin="100" dur="3" fill="freeze"/> + <animateTransform attributeName="transform" type="scale" from="1 1" to="0.5 1" begin="100" dur="3" additive="sum" fill="freeze"/> + </use> + + <use xlink:href="#circleID"> + <animate attributeName="x" from="10" to="70" begin="100" dur="3" fill="freeze"/> + </use> + + <use xlink:href="#polylineID" transform=""> + <animateTransform attributeName="transform" type="rotate" from="0" to="15" additive="sum" begin="100" dur="3" fill="freeze"/> + </use> + + <use x="60" y="-10" xlink:href="#polygonID" transform=""> + <animateMotion path="M 0 0 l 0 150" begin="100" dur="3" fill="freeze"/> + <animateTransform attributeName="transform" type="scale" from="1 1" to="1 2" begin="100" dur="3" additive="sum" fill="freeze"/> + </use> + + <use xlink:href="#imageID" transform=""> + <animateTransform attributeName="transform" type="scale" from="1 .25" to="1 1" begin="100" dur="3" additive="sum" fill="freeze"/> + </use> + <!-- END OF ACTUAL TEST CONTENT--> + </g> + + <text id="revision" x="10" y="340" font-size="40" stroke="none" fill="black">$Revision: 1.6 $</text> + <rect id="test-frame" x="1" y="1" width="478" height="358" fill="none" stroke="#000000"/> +</svg> diff --git a/dom/smil/crashtests/523188-1.svg b/dom/smil/crashtests/523188-1.svg new file mode 100644 index 0000000000..c03cea4923 --- /dev/null +++ b/dom/smil/crashtests/523188-1.svg @@ -0,0 +1,15 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait" + onload="setTimeout(removeNode, 0)"> + <script> + function removeNode() { + var node = document.getElementById("myRect"); + node.parentNode.removeChild(node); + document.documentElement.removeAttribute("class"); + } + </script> + <rect id="myRect" x="20" y="20" height="50" width="50" stroke="blue"> + <animate attributeName="stroke-width" from="1" to="9" begin="0s" dur="2s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/525099-1.svg b/dom/smil/crashtests/525099-1.svg new file mode 100644 index 0000000000..8eed11489a --- /dev/null +++ b/dom/smil/crashtests/525099-1.svg @@ -0,0 +1,7 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <rect x="20" y="20" height="50" width="50" fill="blue"> + <animate attributeName="display" by="inline" + begin="0s" dur="1s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/526536-1.svg b/dom/smil/crashtests/526536-1.svg new file mode 100644 index 0000000000..4fcf35d081 --- /dev/null +++ b/dom/smil/crashtests/526536-1.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait" + onload="setTimeout('boom()', 0)"> +<script type="text/javascript"> +<![CDATA[ +function boom() +{ + document.getElementById("anim").setAttribute("fill", "freeze"); + document.documentElement.removeAttribute("class"); +} +]]> +</script> + <g transform="translate(50 50)"> + <circle r="40" style="fill: yellow; stroke: black; stroke-width: 1"> + <animate id="anim" attributeName="cx" attributeType="XML" + values="0; 200" dur="2s" begin="-1s" repeatCount="0.5"/> + </circle> + </g> +</svg> diff --git a/dom/smil/crashtests/526875-1.svg b/dom/smil/crashtests/526875-1.svg new file mode 100644 index 0000000000..281454bf61 --- /dev/null +++ b/dom/smil/crashtests/526875-1.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <animate attributeName="fill-opacity" by="-1"/> +</svg> diff --git a/dom/smil/crashtests/526875-2.svg b/dom/smil/crashtests/526875-2.svg new file mode 100644 index 0000000000..73c229da5f --- /dev/null +++ b/dom/smil/crashtests/526875-2.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> + <animate attributeName="fill-opacity" by="1"/> +</svg> diff --git a/dom/smil/crashtests/529387-1-helper.svg b/dom/smil/crashtests/529387-1-helper.svg new file mode 100644 index 0000000000..7885ab71fd --- /dev/null +++ b/dom/smil/crashtests/529387-1-helper.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <text y="20pt">abc + <animate attributeName="opacity" from="1" to="0" begin="0s" dur="2s"/> + </text> +</svg> diff --git a/dom/smil/crashtests/529387-1.xhtml b/dom/smil/crashtests/529387-1.xhtml new file mode 100644 index 0000000000..de3dbec34c --- /dev/null +++ b/dom/smil/crashtests/529387-1.xhtml @@ -0,0 +1,7 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + <script> + var p = new XMLHttpRequest(); + p.open("GET", "529387-1-helper.svg", false); + p.send(); + </script> +</html> diff --git a/dom/smil/crashtests/531550-1.svg b/dom/smil/crashtests/531550-1.svg new file mode 100644 index 0000000000..306f41702d --- /dev/null +++ b/dom/smil/crashtests/531550-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <g><animateTransform attributeName="transform" by="1"/></g> +</svg> diff --git a/dom/smil/crashtests/541297-1.svg b/dom/smil/crashtests/541297-1.svg new file mode 100644 index 0000000000..4268232ba1 --- /dev/null +++ b/dom/smil/crashtests/541297-1.svg @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg xmlns="http://www.w3.org/2000/svg"><svg id="w"><animate/></svg><script type="text/javascript"> +<![CDATA[ + +function boom() +{ + anim = document.createElementNS("http://www.w3.org/2000/svg", "animate"); + document.documentElement.appendChild(anim); + document.documentElement.removeChild(anim); + + setTimeout(t, 0); + + function t() + { + document.getElementById("w").appendChild(anim); + } +} + +window.addEventListener("load", boom, false); + +]]> +</script></svg> diff --git a/dom/smil/crashtests/547333-1.svg b/dom/smil/crashtests/547333-1.svg new file mode 100644 index 0000000000..bac629b493 --- /dev/null +++ b/dom/smil/crashtests/547333-1.svg @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + document.getElementsByTagName("animate")[0].setAttributeNS(null, "attributeName", "font-size"); + document.getElementsByTagName("text")[0].setAttributeNS(null, "fill", "green"); + document.documentElement.removeAttributeNS(null, "x"); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +<text>abc<animate/></text> + +</svg> diff --git a/dom/smil/crashtests/548899-1.svg b/dom/smil/crashtests/548899-1.svg new file mode 100644 index 0000000000..c12ed27454 --- /dev/null +++ b/dom/smil/crashtests/548899-1.svg @@ -0,0 +1,14 @@ +<?xml version="1.0"?>
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <circle id="circleID" cx="20" cy="100" r="10" fill="orange" stroke="black">
+ <animateTransform attributeName="transform" type="scale"
+ from="1" to="2" begin="0" dur="3"/>
+ </circle>
+ <rect id="rectID" fill="green" stroke="black" height="100" width="100">
+ <animate attributeName="fill" from="white" to="blue" begin="0" dur="3"/>
+ </rect>
+ </defs>
+ <use xlink:href="#circleID"/>
+</svg>
diff --git a/dom/smil/crashtests/551620-1.svg b/dom/smil/crashtests/551620-1.svg new file mode 100644 index 0000000000..2ea83e9c29 --- /dev/null +++ b/dom/smil/crashtests/551620-1.svg @@ -0,0 +1,21 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + +<animate id="x" begin="y.end"/> +<animate id="y"/> + +<script> + +function boom() +{ + var x = document.getElementById("x"); + var y = document.getElementById("y"); + y.appendChild(x); + y.setAttributeNS(null, "dur", "0.5s"); + y.removeAttributeNS(null, "dur"); +} + +window.addEventListener("load", boom, false); + +</script> + +</svg> diff --git a/dom/smil/crashtests/554141-1.svg b/dom/smil/crashtests/554141-1.svg new file mode 100644 index 0000000000..61ce419f53 --- /dev/null +++ b/dom/smil/crashtests/554141-1.svg @@ -0,0 +1,12 @@ +<svg xmlns="http://www.w3.org/2000/svg" + onload=" + document.documentElement.pauseAnimations(); + document.documentElement.setCurrentTime(0); + document.getElementById('b').removeAttribute('begin'); + document.getElementById('a').setAttribute('dur', '1s')"> + <rect> + <animate attributeName="y" attributeType="XML" id="a"/> + <animate attributeName="fill" attributeType="CSS" id="b" + begin="a.end" dur="2s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/554202-2.svg b/dom/smil/crashtests/554202-2.svg new file mode 100644 index 0000000000..a3bbb3195c --- /dev/null +++ b/dom/smil/crashtests/554202-2.svg @@ -0,0 +1,19 @@ +<svg xmlns="http://www.w3.org/2000/svg" + onload=" + document.documentElement.pauseAnimations(); + document.documentElement.setCurrentTime(0); + document.getElementById('a').beginElementAt(1); + document.documentElement.setCurrentTime(2)"> + <!-- + This test case sets up a cycle between simultaneous instance times such that + when the instance times are sorted if this cycle is not detected we will + crash. + --> + <rect width="100" height="100" fill="red"> + <set attributeName="fill" to="blue" begin="a.begin" dur="4s"/> + <set attributeName="fill" to="green" id="a" + begin="b.begin; 3s" dur="4s"/> + <set attributeName="fill" to="red" id="b" + begin="a.begin" dur="4s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/555026-1.svg b/dom/smil/crashtests/555026-1.svg new file mode 100644 index 0000000000..76b4cf0756 --- /dev/null +++ b/dom/smil/crashtests/555026-1.svg @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait" + onload="go()"> + <script> + function go() { + // setCurrentTime to force a sample + document.documentElement.setCurrentTime(1); + document.documentElement.removeAttribute("class"); + } + </script> + <rect id="myRect" fill="blue" height="40" width="40"> + <!-- The "keyTimes" values below are invalid, but they should be ignored + (and definitely shouldn't trigger any assertion failures) since we're + in paced calcMode. --> + <animate attributeName="x" by="50" calcMode="paced" dur="2s" + keyTimes="0; -1"/> + <animate attributeName="x" by="50" calcMode="paced" dur="2s" + keyTimes=""/> + <animate attributeName="x" by="50" calcMode="paced" dur="2s" + keyTimes="abc"/> + <animate attributeName="x" by="50" calcMode="paced" dur="2s" + keyTimes="5"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/556841-1.svg b/dom/smil/crashtests/556841-1.svg new file mode 100644 index 0000000000..92712deaa9 --- /dev/null +++ b/dom/smil/crashtests/556841-1.svg @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait" + onload="go()"> + <script> + function go() { + // setCurrentTime to force a sample + document.documentElement.setCurrentTime(2); + document.documentElement.removeAttribute("class"); + } + </script> + <rect fill="teal" x="50" y="50" width="20" height="20"> + <animateTransform attributeName="transform" type="rotate" by="30" + calcMode="paced" dur="4s"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/572938-1.svg b/dom/smil/crashtests/572938-1.svg new file mode 100644 index 0000000000..d759944c7d --- /dev/null +++ b/dom/smil/crashtests/572938-1.svg @@ -0,0 +1,12 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <text id="myText">Used Text Element + <set attributeName="display" to="none"/> + </text> + </defs> + <use xlink:href="#myText" x="20" y="40"/> + <text x="20" y="60">Normal Text Element + <set attributeName="display" to="none"/> + </text> +</svg> diff --git a/dom/smil/crashtests/572938-2.svg b/dom/smil/crashtests/572938-2.svg new file mode 100644 index 0000000000..8b9cf7b70e --- /dev/null +++ b/dom/smil/crashtests/572938-2.svg @@ -0,0 +1,22 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink" + class="reftest-wait"> + + <script> + function boom() + { + document.getElementById("circleID").removeChild( + document.getElementById("at")); + document.documentElement.removeAttribute("class"); + } + window.addEventListener("load", boom, false); + </script> + + <circle id="circleID"> + <animate/> + <animateTransform id="at" attributeName="transform"/> + </circle> + <animate attributeName="stroke-width"/> + <use xlink:href="#circleID"/> + +</svg> diff --git a/dom/smil/crashtests/572938-3.svg b/dom/smil/crashtests/572938-3.svg new file mode 100644 index 0000000000..642ad32fba --- /dev/null +++ b/dom/smil/crashtests/572938-3.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <defs> + <text id="a">Text A</text> + <text id="b">Text B</text> + </defs> + <use xlink:href="#a" x="20" y="40"> + <set attributeName="xlink:href" to="#b" dur="2s"/> + </use> +</svg> diff --git a/dom/smil/crashtests/572938-4.svg b/dom/smil/crashtests/572938-4.svg new file mode 100644 index 0000000000..549d43dd62 --- /dev/null +++ b/dom/smil/crashtests/572938-4.svg @@ -0,0 +1,10 @@ +<svg xmlns="http://www.w3.org/2000/svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> +<g id="a"> + <path d=""><animate/></path> +</g> +<g display="none"> +<use xlink:href="#a" x="80"/> +<set attributeName="display" to="inline"/> +</g> +</svg> diff --git a/dom/smil/crashtests/588287-1.svg b/dom/smil/crashtests/588287-1.svg new file mode 100644 index 0000000000..cc35cf6b46 --- /dev/null +++ b/dom/smil/crashtests/588287-1.svg @@ -0,0 +1,24 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> + +function boom() +{ + var animate = document.createElementNS("http://www.w3.org/2000/svg", "animate"); + animate.setAttributeNS(null, "begin", "0.5s"); + document.documentElement.appendChild(animate); + + setTimeout(function() { + var g = document.createElement("g"); + var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + g.appendChild(svg); + document.documentElement.appendChild(g); + svg.appendChild(animate); + document.documentElement.removeAttribute("class"); + }, 400); +} + +window.addEventListener("load", function() { setTimeout(boom, 200) }, false); + +</script> +</svg> diff --git a/dom/smil/crashtests/588287-2.svg b/dom/smil/crashtests/588287-2.svg new file mode 100644 index 0000000000..70d8e76391 --- /dev/null +++ b/dom/smil/crashtests/588287-2.svg @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> + +function boom() +{ + var animate = document.createElementNS("http://www.w3.org/2000/svg", "animate"); + animate.setAttributeNS(null, "begin", "0.5s"); + document.documentElement.appendChild(animate); + + setTimeout(function() { + var g = document.createElement("g"); + var svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + g.appendChild(svg); + document.documentElement.appendChild(g); + svg.setCurrentTime(0.2); + svg.appendChild(animate); + svg.setCurrentTime(0.0); // Trigger backwards seek + document.documentElement.removeAttribute("class"); + }, 400); +} + +window.addEventListener("load", function() { setTimeout(boom, 200) }, false); + +</script> +</svg> diff --git a/dom/smil/crashtests/590425-1.html b/dom/smil/crashtests/590425-1.html new file mode 100644 index 0000000000..906d348db2 --- /dev/null +++ b/dom/smil/crashtests/590425-1.html @@ -0,0 +1,24 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> + +function boom() +{ + var frame = document.getElementById("frame") + var frameSVG = frame.contentDocument.getElementById('s'); + var animate = frame.contentDocument.createElementNS("http://www.w3.org/2000/svg", "animate"); + frame.remove(); + frameSVG.appendChild(animate); + document.documentElement.removeAttribute("class"); +} + +</script> +</head> + +<body onload="boom()"> + +<iframe id="frame" srcdoc="<body><svg id=s>"></iframe> + +</body> +</html> diff --git a/dom/smil/crashtests/594653-1.svg b/dom/smil/crashtests/594653-1.svg new file mode 100644 index 0000000000..76352ce30b --- /dev/null +++ b/dom/smil/crashtests/594653-1.svg @@ -0,0 +1,26 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +<![CDATA[ + +window.addEventListener("load", boom, false); + +function boom() +{ + setTimeout(bang, 0); +} + +function bang() +{ + document.documentElement.setCurrentTime(0); + document.documentElement.removeAttribute("class"); +} + +]]> +</script> + +<animate id="b"/> +<animate end="b.end"/> + +</svg> diff --git a/dom/smil/crashtests/596796-1.svg b/dom/smil/crashtests/596796-1.svg new file mode 100644 index 0000000000..52a66fd582 --- /dev/null +++ b/dom/smil/crashtests/596796-1.svg @@ -0,0 +1,15 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> + +<script> +function boom() +{ + document.documentElement.appendChild(document.getElementById("a")); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); +</script> + +<animate end="a.begin" id="a"/> + +</svg> diff --git a/dom/smil/crashtests/605345-1.svg b/dom/smil/crashtests/605345-1.svg new file mode 100644 index 0000000000..94887cf713 --- /dev/null +++ b/dom/smil/crashtests/605345-1.svg @@ -0,0 +1,25 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +<![CDATA[ + +function boom() +{ + var anim = document.getElementById("a"); + var newSvg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + var oldSvg = document.removeChild(document.documentElement); + document.appendChild(newSvg); + document.removeChild(document.documentElement); + newSvg.pauseAnimations(); + document.appendChild(newSvg); + newSvg.appendChild(anim); + + oldSvg.removeAttribute("class"); +} + +window.addEventListener("load", function() { setTimeout(boom, 200); }, false); + +]]> +</script> +<animate id="a"/> +</svg> diff --git a/dom/smil/crashtests/606101-1.svg b/dom/smil/crashtests/606101-1.svg new file mode 100644 index 0000000000..988c86fa33 --- /dev/null +++ b/dom/smil/crashtests/606101-1.svg @@ -0,0 +1,23 @@ +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> +<script> + +function boom() +{ + var origSVG = document.documentElement; + + var a = document.createElementNS("http://www.w3.org/2000/svg", "animate"); + var g = document.createElementNS("http://www.w3.org/2000/svg", "g"); + var s = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + document.removeChild(document.documentElement); + document.appendChild(g); + s.appendChild(a); + g.appendChild(s); + + origSVG.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +</script> +</svg> diff --git a/dom/smil/crashtests/608295-1.html b/dom/smil/crashtests/608295-1.html new file mode 100644 index 0000000000..354e6f9099 --- /dev/null +++ b/dom/smil/crashtests/608295-1.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<html class="reftest-wait"> +<head> +<script> + +function boom() +{ + // NB: <script src> is needed to trigger the bug. I'm being clever by also using it to remove reftest-wait. + var s = "<script src='data:text/javascript,parent.document.documentElement.className=null;'><\/script><svg>"; + document.getElementById("f").contentDocument.write(s); +} + +</script> +</head> +<body onload="boom();"> +<iframe id="f"></iframe> +</body> +</html> diff --git a/dom/smil/crashtests/608549-1.svg b/dom/smil/crashtests/608549-1.svg new file mode 100644 index 0000000000..dd441e0135 --- /dev/null +++ b/dom/smil/crashtests/608549-1.svg @@ -0,0 +1,29 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +<![CDATA[ + +function boom() +{ + try { + document.getElementById("set").beginElementAt(NaN); + return; + } catch (e) {} + try { + document.getElementById("set").endElementAt(NaN); + return; + } catch (e) {} + + // If we got here we threw both exceptions and skipped both early-returns, as + // expected. + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +<set id="set" attributeName="fill" to="green" begin="indefinite"/> + +</svg> diff --git a/dom/smil/crashtests/611927-1.svg b/dom/smil/crashtests/611927-1.svg new file mode 100644 index 0000000000..ea60f4ce1c --- /dev/null +++ b/dom/smil/crashtests/611927-1.svg @@ -0,0 +1,4 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <animate attributeName="stroke-width"/> + <animate attributeName="stroke-width" by="10em"/> +</svg> diff --git a/dom/smil/crashtests/615002-1.svg b/dom/smil/crashtests/615002-1.svg new file mode 100644 index 0000000000..eb9a293199 --- /dev/null +++ b/dom/smil/crashtests/615002-1.svg @@ -0,0 +1,16 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +function boom() +{ + var a = document.getElementById("a"); + a.removeAttribute('dur'); + document.documentElement.appendChild(a); + // Force a sample + document.documentElement.setCurrentTime(0); + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); +</script> +<animate begin="-2s" dur="2s" id="a"/> +</svg> diff --git a/dom/smil/crashtests/615872-1.svg b/dom/smil/crashtests/615872-1.svg new file mode 100644 index 0000000000..e0cdf21546 --- /dev/null +++ b/dom/smil/crashtests/615872-1.svg @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"><script> +<![CDATA[ + +function boom() +{ + var r = document.documentElement; + var s = document.createElementNS("http://www.w3.org/2000/svg", "set"); + s.setAttributeNS(null, "begin", "1s"); + r.appendChild(s); + r.setCurrentTime(2); + document.removeChild(r); + r.setCurrentTime(0); + s.beginElementAt(0); +} + +window.addEventListener("load", boom, false); + +]]> +</script></svg> diff --git a/dom/smil/crashtests/641388-1.html b/dom/smil/crashtests/641388-1.html new file mode 100644 index 0000000000..25c941dedb --- /dev/null +++ b/dom/smil/crashtests/641388-1.html @@ -0,0 +1,97 @@ +<script>
+
+var ar = new Array(100000);
+
+function fill() {
+ var s = unescape("%ubeef%udead%udead%udead%u0000%u0000%u3030%u3030");
+ while(s.length < 0x40000) {
+ for(var x=0; x<100; x++) ar.push(s+s);
+ s+=s;
+ }
+}
+
+
+function gc() {
+ var evt = document.createEvent("Events");
+ evt.initEvent("please-gc", true, false);
+ document.dispatchEvent(evt);
+ fill();
+}
+
+
+gc();
+function start(){
+tmp = document.createElement('iframe');
+tmp.src="data:image/svg+xml,"+escape("<?xml version='1.0' standalone='no'?><!DOCTYPE svg><svg xmlns='http://www.w3.org/2000/svg'><defs id='element1'></defs><g id='element5'></g></svg>");
+tmp.id = 'ifr23282';
+try{document.getElementById('store_div').appendChild(tmp);}catch(e){}
+window.setTimeout('startrly()', 100);
+} function startrly() {
+try{o6=document.createComment(null);}catch(e){}
+try{o9=document.getElementById('ifr23282').contentDocument.documentElement;;}catch(e){}
+try{o13=document.getElementById('ifr23282').contentDocument.getElementById('element1');;}catch(e){}
+try{o15=document.getElementById('ifr23282').contentDocument.getElementById('element5');;}catch(e){}
+try{tmp = document.createElement('iframe');}catch(e){}
+try{tmp.id = 'ifr6690';}catch(e){}
+try{o6.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe0()',100);
+} function start_dataiframe0(){
+try{o19=o6.ownerDocument.getElementById('ifr6690').contentDocument.documentElement;;}catch(e){}
+try{o24=document.createElementNS('http://www.w3.org/1998/Math/MathML','annotation-xml');;}catch(e){}
+try{o35=document.createElementNS('http://www.w3.org/1998/Math/MathML','emptyset');;}catch(e){}
+try{o40=o19.cloneNode(false);;}catch(e){}
+try{o19.appendChild(o13);}catch(e){}
+try{o19.appendChild(o15);}catch(e){}
+try{o24.appendChild(o40);}catch(e){}
+try{tmp = document.createElement('iframe');}catch(e){}
+tmp.src="data:text/html,<article%20id='element1'></article><command%20id='element3'></command>";
+try{tmp.id = 'ifr17516';}catch(e){}
+try{o13.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe4()',100);
+} function start_dataiframe4(){
+try{o62=o13.ownerDocument.getElementById('ifr17516').contentDocument.getElementById('element1');;}catch(e){}
+try{tmp.id = 'ifr2522';}catch(e){}
+try{o101=o15.ownerDocument.getElementById('ifr2522').contentDocument.getElementById('element3');;}catch(e){}
+try{o101.appendChild(o24);}catch(e){}
+try{o112=document.createElementNS('http://www.w3.org/1999/xhtml', 'script');;}catch(e){}
+try{o124=document.createElementNS('http://www.w3.org/1998/Math/MathML','root');;}catch(e){}
+try{o125=document.createElementNS('http://www.w3.org/2000/svg','font-face');;}catch(e){}
+gc()
+try{o150=o40;}catch(e){}
+try{tmp.id = 'ifr44501';}catch(e){}
+try{o124.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe7()',100);
+} function start_dataiframe7(){
+try{o152=o124.ownerDocument.getElementById('ifr44501').contentDocument.documentElement;;}catch(e){}
+try{tmp = document.createElement('iframe');}catch(e){}
+try{tmp.src="data:text/html,<div%20id='element1'></div>";}catch(e){}
+try{tmp.id = 'ifr55543';}catch(e){}
+try{o125.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe10()',100);
+} function start_dataiframe10(){
+try{o198=o125.ownerDocument.getElementById('ifr55543').contentDocument.getElementById('element1');;}catch(e){}
+try{o152.appendChild(o101);}catch(e){}
+try{o152.ownerDocument.documentElement.appendChild(tmp);}catch(e){}
+window.setTimeout('start_dataiframe17()',100);
+} function start_dataiframe17(){
+try{o286=o152.ownerDocument.getElementById('ifr55543').contentDocument.documentElement;;}catch(e){}
+try{o288=o152.ownerDocument.getElementById('ifr55543').contentDocument.getElementById('element1');;}catch(e){}
+try{o349=document.createElementNS('http://www.w3.org/2000/svg','animate');;}catch(e){}
+try{o150.appendChild(o349);}catch(e){}
+try{o288.appendChild(o150);}catch(e){}
+try{o198.appendChild(o349);}catch(e){}
+window.setTimeout('start_dataiframe24()',100);
+} function start_dataiframe24(){
+try{o286.appendChild(o9);}catch(e){}
+try{o62.appendChild(o152);}catch(e){}
+try{o112.appendChild(o286);}catch(e){}
+try{o534=o35.cloneNode(false);;}catch(e){}
+gc();
+o35 = null;
+gc();
+window.setTimeout("fill()",300);
+}
+</script>
+<body onload="start()">
+<div id="store_div"></div>
+</body>
diff --git a/dom/smil/crashtests/641388-2.html b/dom/smil/crashtests/641388-2.html new file mode 100644 index 0000000000..f2ddead7e1 --- /dev/null +++ b/dom/smil/crashtests/641388-2.html @@ -0,0 +1,79 @@ +<script>
+function gc() {
+ var evt = document.createEvent("Events");
+ evt.initEvent("please-gc", true, false);
+ document.dispatchEvent(evt);
+ }
+var ar =new Array(100000);
+function fill() {
+ var s = unescape("%u0000%u0000%u3030%u3030");
+ while(s.length < 0x40000) {
+ for(var x=0; x<100; x++) ar.push(s+s);
+ s+=s;
+ }
+}
+
+
+function start(){
+tmp = document.createElement('iframe'); 'ifr16727';
+document.documentElement.appendChild(tmp);
+window.setTimeout('start_dataiframe0()',100);
+} function start_dataiframe0(){
+o20=document.createElement('iframe');
+tmp.id = 'ifr4446';;
+o68=o20;
+o101=document.getElementById('ifr4446').contentDocument.createElement('thead');;
+tmp.src="data:text/html," + escape("<html id='element0'><noscript id='element1'></html>");
+tmp.id = 'ifr49879';
+window.setTimeout('start_dataiframe6()',100);
+} function start_dataiframe6(){
+o104=document.getElementById('ifr49879').contentDocument.getElementById('element0');;
+o105=document.getElementById('ifr49879').contentDocument.getElementById('element1');;
+o120=document.getElementById('ifr49879').contentDocument.createElement('figure');;
+o105.appendChild(o120);
+o122=o105.lastElementChild;
+o140=document.getElementById('ifr49879').contentDocument.createElement('style');;
+o141=document.getElementById('ifr49879').contentDocument.createElementNS('http://www.w3.org/2000/svg','animate');;
+o151=o141.cloneNode(true);;
+tmp = document.createElement('iframe');
+tmp.src='data:text/html,%3Cform%20style%3B%27%20id%3D%27element3%27%3E%20%3Caside%20style%20id%3D%27element4%27%%3E';
+tmp.id = 'ifr13645';
+document.documentElement.appendChild(tmp);
+window.setTimeout('start_dataiframe8()',100);
+} function start_dataiframe8(){
+o154=document.getElementById('ifr13645').contentDocument.documentElement;;
+o158=document.getElementById('ifr13645').contentDocument.getElementById('element3');;
+o159=document.getElementById('ifr13645').contentDocument.getElementById('element4');;
+tmp.id = 'ifr17164';
+o120.ownerDocument.documentElement.appendChild(tmp);
+o171=o120.ownerDocument.getElementById('ifr17164').contentDocument.documentElement;;
+tmp = o158.ownerDocument.createElement('iframe');
+o101.appendChild(o151);
+o122.appendChild(o154);
+o68.appendChild(o171);
+o179=document.createElement('tbody');;
+o154.addEventListener('DOMNodeRemoved',function (event) { gc(); });
+tmp.src='data:text/html,%3Cs%27%20id%3D%27element0%27element4%27%3E%3Cs%20id%3D%27element5%27%20style%3D%27text-indent%3A%20-1%25%3Bmin-w%2C%20rgba%28255%2C0%2C0%2C0%29%20strict%3Bcolumn-count7element9%27%3E%s%3E';
+tmp.id = 'ifr35960';
+o154.ownerDocument.documentElement.appendChild(tmp);
+window.setTimeout('start_dataiframe13()',100);
+} function start_dataiframe13(){
+o217=o154.ownerDocument.getElementById('ifr35960').contentDocument.documentElement;;
+o218=o154.ownerDocument.getElementById('ifr35960').contentDocument.getElementById('element0');;
+o223=o154.ownerDocument.getElementById('ifr35960').contentDocument.getElementById('element5');;
+o223.appendChild(o101);
+o218.appendChild(o140);
+o140.appendChild(o151);
+o104.appendChild(o179);
+o230=o120.ownerDocument.getElementById('ifr17164').contentDocument.createElementNS('http://www.w3.org/2000/svg','svg');;
+window.setTimeout('start_dataiframe14()',100);
+} function start_dataiframe14(){
+gc();fill();
+o140.appendChild(o230);
+o171.appendChild(o104);
+o159.appendChild(o217);
+o158.appendChild(o218);
+}
+window.setTimeout("start()",100);
+</script>
+
diff --git a/dom/smil/crashtests/650732-1.svg b/dom/smil/crashtests/650732-1.svg new file mode 100644 index 0000000000..95be31c16a --- /dev/null +++ b/dom/smil/crashtests/650732-1.svg @@ -0,0 +1,46 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> + <rect fill="green" width="100" height="100"> + <set id="a" attributeName="fill" to="blue" + begin="6s" end="986s"/> + <set id="b" attributeName="fill" to="orange" + begin="a.begin+69.3s;b.begin+700s" dur="700s" end="a.end"/> + <set id="c" attributeName="fill" to="yellow" + begin="0s;b.begin+700s"/> + </rect> + <script type="text/javascript"> +<![CDATA[ +const max_attempts = 100; +var attempts = 0; +function attemptCrash() +{ + remove(); + add(); + if (++attempts >= max_attempts) { + document.documentElement.removeAttribute("class"); + } else { + setTimeout(attemptCrash, 0); + } +} +function add() +{ + const svgns = "http://www.w3.org/2000/svg"; + var elem = document.createElementNS(svgns, "set"); + elem.setAttribute("id", "b"); + elem.setAttribute("attributeName", "fill"); + elem.setAttribute("to", "orange"); + elem.setAttribute("begin", "a.begin+69.3s;b.begin+700s"); + elem.setAttribute("dur", "700s"); + elem.setAttribute("end", "a.end"); + rect = document.getElementsByTagNameNS(svgns, "rect")[0]; + rect.appendChild(elem); +} +function remove() +{ + var elem = document.getElementById('b'); + elem.parentNode.removeChild(elem); + elem = null; +} +window.addEventListener("load", attemptCrash, false); +]]> + </script> +</svg> diff --git a/dom/smil/crashtests/665334-1.svg b/dom/smil/crashtests/665334-1.svg new file mode 100644 index 0000000000..94916d1e0e --- /dev/null +++ b/dom/smil/crashtests/665334-1.svg @@ -0,0 +1,13 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> +function boom() +{ + // Remove the first 'a'. + document.documentElement.removeChild(document.getElementById("a")); + document.documentElement.removeAttribute("class"); +} +window.addEventListener("load", boom, false); +</script> +<animate id="a"/> +<animate id="a" end="a.begin" /> +</svg> diff --git a/dom/smil/crashtests/669225-1.svg b/dom/smil/crashtests/669225-1.svg new file mode 100644 index 0000000000..9660105631 --- /dev/null +++ b/dom/smil/crashtests/669225-1.svg @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"> + +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + document.documentElement.appendChild(document.getElementById("a")); +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +<animate end="a.begin" id="a"/> +<animate end="a.begin" id="a"/> + +</svg> diff --git a/dom/smil/crashtests/669225-2.svg b/dom/smil/crashtests/669225-2.svg new file mode 100644 index 0000000000..00d52c1f4c --- /dev/null +++ b/dom/smil/crashtests/669225-2.svg @@ -0,0 +1,21 @@ +<?xml version="1.0"?> + +<svg xmlns="http://www.w3.org/2000/svg"> + +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + var a = document.getElementById("a"); + a.removeAttribute("end"); + a.setAttribute("end", "a.begin"); +} + +window.addEventListener("load", boom, false); + +]]> +</script> + +<animate end="0" id="a" onend="boom()"/> +</svg> diff --git a/dom/smil/crashtests/670313-1.svg b/dom/smil/crashtests/670313-1.svg new file mode 100644 index 0000000000..97e12f35ac --- /dev/null +++ b/dom/smil/crashtests/670313-1.svg @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" + class="reftest-wait"> +<script type="text/javascript"> +<![CDATA[ + +function boom() +{ + try { + document.getElementById("x").beginElementAt(36028797018963970); + } catch (e) { } + document.documentElement.removeAttribute("class"); +} + +window.addEventListener("load", boom, false); + +]]> +</script> +<animate id="x" begin="a" /> +</svg> diff --git a/dom/smil/crashtests/678822-1.svg b/dom/smil/crashtests/678822-1.svg new file mode 100644 index 0000000000..a5e81ee10f --- /dev/null +++ b/dom/smil/crashtests/678822-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <animate repeatCount="2" dur="1s" accumulate="1" /> +</svg> diff --git a/dom/smil/crashtests/678847-1.svg b/dom/smil/crashtests/678847-1.svg new file mode 100644 index 0000000000..1fa2718cbb --- /dev/null +++ b/dom/smil/crashtests/678847-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> +<animate id="a" end="a.end+6s" /> +</svg> diff --git a/dom/smil/crashtests/678938-1.svg b/dom/smil/crashtests/678938-1.svg new file mode 100644 index 0000000000..f3f8308fa5 --- /dev/null +++ b/dom/smil/crashtests/678938-1.svg @@ -0,0 +1,11 @@ +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait"> +<script> + window.addEventListener("load", function() { + setTimeout(function() { + document.documentElement.setCurrentTime(0); + document.documentElement.removeAttribute("class"); + }, 0); + }, false); +</script> +<set id="c"/><set id="b" begin="c.begin; b.begin"/> +</svg> diff --git a/dom/smil/crashtests/690994-1.svg b/dom/smil/crashtests/690994-1.svg new file mode 100644 index 0000000000..252fd2c264 --- /dev/null +++ b/dom/smil/crashtests/690994-1.svg @@ -0,0 +1,17 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" class="reftest-wait" onload="go()"> +<script> +<![CDATA[ +function go() { + document.documentElement.setCurrentTime(100); +} +function boom() +{ + document.documentElement.removeChild(document.getElementById("a")); + document.documentElement.removeAttribute("class"); +} +]]> +</script> +<animate id="a" begin="a.end; 99.9s" end="a.begin+0.2s" onend="boom()"/> +<animate id="a" begin="a.end; 99.9s" end="a.begin+0.2s"/> +</svg> diff --git a/dom/smil/crashtests/691337-1.svg b/dom/smil/crashtests/691337-1.svg new file mode 100644 index 0000000000..c341faa6b2 --- /dev/null +++ b/dom/smil/crashtests/691337-1.svg @@ -0,0 +1,8 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> + <rect width="100" height="100" fill="blue"> + <animate attributeName="fill" + begin="999999999999999999999999999999999999999999999999999999999999999999999999999999999" + dur="5s" from="blue" to="red" repeatCount="indefinite" additive="sum"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/691337-2.svg b/dom/smil/crashtests/691337-2.svg new file mode 100644 index 0000000000..f4408ae5ee --- /dev/null +++ b/dom/smil/crashtests/691337-2.svg @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"> + <rect width="100" height="100" fill="blue"> + <animate attributeName="fill" id="a" + begin="4999999999999999" dur="5s" from="blue" to="red" + repeatCount="indefinite" additive="sum"/> + <animate attributeName="fill" + begin="a.begin+4999999999999999" + dur="5s" from="blue" to="red" repeatCount="indefinite" additive="sum"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/697640-1.svg b/dom/smil/crashtests/697640-1.svg new file mode 100644 index 0000000000..c2e1b89fdb --- /dev/null +++ b/dom/smil/crashtests/697640-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <animate id="b" end="b.end" dur="3s" /> +</svg> diff --git a/dom/smil/crashtests/699325-1.svg b/dom/smil/crashtests/699325-1.svg new file mode 100644 index 0000000000..7496c6ae21 --- /dev/null +++ b/dom/smil/crashtests/699325-1.svg @@ -0,0 +1,5 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <rect fill="blue" height="100" width="100"> + <animate id="a" attributeName="x" calcMode="paced" values="50; 50; 50"/> + </rect> +</svg> diff --git a/dom/smil/crashtests/709907-1.svg b/dom/smil/crashtests/709907-1.svg new file mode 100644 index 0000000000..631911970c --- /dev/null +++ b/dom/smil/crashtests/709907-1.svg @@ -0,0 +1,3 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <animate attributeName="stroke-dasharray" from="-3" /> +</svg> diff --git a/dom/smil/crashtests/720103-1.svg b/dom/smil/crashtests/720103-1.svg new file mode 100644 index 0000000000..a51a3bf0fc --- /dev/null +++ b/dom/smil/crashtests/720103-1.svg @@ -0,0 +1,4 @@ +<?xml version="1.0"?> +<svg xmlns="http://www.w3.org/2000/svg"> +<animate id="a" begin="-3.1s" end="a.begin+0.2s"/> +</svg> diff --git a/dom/smil/crashtests/849593-1.xhtml b/dom/smil/crashtests/849593-1.xhtml new file mode 100644 index 0000000000..95b9b2feb8 --- /dev/null +++ b/dom/smil/crashtests/849593-1.xhtml @@ -0,0 +1,34 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head class="reftest-wait"> +<meta charset="utf-8"/> +<script> +<![CDATA[ + +function boom() +{ + var a = document.getElementById('a'); + var b = document.getElementById('b'); + var c = document.getElementById('c'); + var d = document.getElementById('d'); + + b.setCurrentTime(1); + + a.appendChild(c); + document.body.removeChild(a); + b.appendChild(c); + document.documentElement.offsetHeight; + d.appendChild(a); + + document.documentElement.removeAttribute('class'); +} + +]]> +</script> +</head> + +<body onload="setTimeout(boom, 0);"> +<svg xmlns="http://www.w3.org/2000/svg" id="a"/> +<svg xmlns="http://www.w3.org/2000/svg" id="b"/> +<set xmlns="http://www.w3.org/2000/svg" begin="1s" id="c"><div xmlns="http://www.w3.org/1999/xhtml" id="d"></div></set> +</body> +</html> diff --git a/dom/smil/crashtests/crashtests.list b/dom/smil/crashtests/crashtests.list new file mode 100644 index 0000000000..f74c364af0 --- /dev/null +++ b/dom/smil/crashtests/crashtests.list @@ -0,0 +1,61 @@ +load 483584-1.svg +load 483584-2.svg +load 523188-1.svg +load 525099-1.svg +load 526536-1.svg +load 526875-1.svg +load 526875-2.svg +load 529387-1.xhtml +load 531550-1.svg +load 541297-1.svg +load 547333-1.svg +load 548899-1.svg +load 551620-1.svg +load 554141-1.svg +load 554202-2.svg +load 555026-1.svg +load 556841-1.svg +load 572938-1.svg +load 572938-2.svg +load 572938-3.svg +load 572938-4.svg +load 588287-1.svg +load 588287-2.svg +load 590425-1.html +load 594653-1.svg +load 596796-1.svg +load 605345-1.svg +load 606101-1.svg +load 608295-1.html +load 608549-1.svg +load 611927-1.svg +load 615002-1.svg +load 615872-1.svg +load 641388-1.html +load 641388-2.html +load 650732-1.svg +load 665334-1.svg +load 669225-1.svg +load 669225-2.svg +load 670313-1.svg +load 678822-1.svg +load 678847-1.svg +load 678938-1.svg +load 690994-1.svg +load 691337-1.svg +load 691337-2.svg +load 697640-1.svg +load 699325-1.svg +load 709907-1.svg +load 720103-1.svg +load 849593-1.xhtml +load 1010681-1.svg +load 1322770-1.svg +load 1322849-1.svg +load 1343357-1.html +load 1375596-1.svg +load 1402547-1.html +load 1411963-1.html +load 1413319-1.html +load 1535388-1.html +load 1780800-1.html diff --git a/dom/smil/moz.build b/dom/smil/moz.build new file mode 100644 index 0000000000..c6c3bd7351 --- /dev/null +++ b/dom/smil/moz.build @@ -0,0 +1,74 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +with Files("**"): + BUG_COMPONENT = ("Core", "SVG") + +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] + +EXPORTS.mozilla += [ + "SMILAnimationController.h", + "SMILAnimationFunction.h", + "SMILAttr.h", + "SMILCompositorTable.h", + "SMILCSSValueType.h", + "SMILInstanceTime.h", + "SMILInterval.h", + "SMILKeySpline.h", + "SMILMilestone.h", + "SMILNullType.h", + "SMILParserUtils.h", + "SMILRepeatCount.h", + "SMILSetAnimationFunction.h", + "SMILTargetIdentifier.h", + "SMILTimeContainer.h", + "SMILTimedElement.h", + "SMILTimeValue.h", + "SMILTimeValueSpec.h", + "SMILTimeValueSpecParams.h", + "SMILType.h", + "SMILTypes.h", + "SMILValue.h", +] + +EXPORTS.mozilla.dom += [ + "TimeEvent.h", +] + +UNIFIED_SOURCES += [ + "SMILAnimationController.cpp", + "SMILAnimationFunction.cpp", + "SMILBoolType.cpp", + "SMILCompositor.cpp", + "SMILCSSProperty.cpp", + "SMILCSSValueType.cpp", + "SMILEnumType.cpp", + "SMILFloatType.cpp", + "SMILInstanceTime.cpp", + "SMILIntegerType.cpp", + "SMILInterval.cpp", + "SMILKeySpline.cpp", + "SMILNullType.cpp", + "SMILParserUtils.cpp", + "SMILRepeatCount.cpp", + "SMILSetAnimationFunction.cpp", + "SMILStringType.cpp", + "SMILTimeContainer.cpp", + "SMILTimedElement.cpp", + "SMILTimeValue.cpp", + "SMILTimeValueSpec.cpp", + "SMILValue.cpp", + "TimeEvent.cpp", +] + +LOCAL_INCLUDES += [ + "/dom/base", + "/dom/svg", + "/layout/base", + "/layout/style", +] + +FINAL_LIBRARY = "xul" diff --git a/dom/smil/test/db_smilAnimateMotion.js b/dom/smil/test/db_smilAnimateMotion.js new file mode 100644 index 0000000000..31c338586f --- /dev/null +++ b/dom/smil/test/db_smilAnimateMotion.js @@ -0,0 +1,309 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* testcase data for <animateMotion> */ + +// Fake motion 'attribute', to satisfy testing code that expects an attribute. +var gMotionAttr = new AdditiveAttribute( + SMILUtil.getMotionFakeAttributeName(), + "XML", + "rect" +); + +// CTM-summary-definitions, for re-use by multiple testcase bundles below. +var _reusedCTMLists = { + pacedBasic: { + ctm0: [100, 200, 0], + ctm1_6: [105, 205, 0], + ctm1_3: [110, 210, 0], + ctm2_3: [120, 220, 0], + ctm1: [130, 210, 0], + }, + pacedR60: { + ctm0: [100, 200, Math.PI / 3], + ctm1_6: [105, 205, Math.PI / 3], + ctm1_3: [110, 210, Math.PI / 3], + ctm2_3: [120, 220, Math.PI / 3], + ctm1: [130, 210, Math.PI / 3], + }, + pacedRAuto: { + ctm0: [100, 200, Math.PI / 4], + ctm1_6: [105, 205, Math.PI / 4], + ctm1_3: [110, 210, Math.PI / 4], + ctm2_3: [120, 220, -Math.PI / 4], + ctm1: [130, 210, -Math.PI / 4], + }, + pacedRAutoReverse: { + ctm0: [100, 200, (5 * Math.PI) / 4], + ctm1_6: [105, 205, (5 * Math.PI) / 4], + ctm1_3: [110, 210, (5 * Math.PI) / 4], + ctm2_3: [120, 220, (3 * Math.PI) / 4], + ctm1: [130, 210, (3 * Math.PI) / 4], + }, + + discreteBasic: { + ctm0: [100, 200, 0], + ctm1_6: [100, 200, 0], + ctm1_3: [120, 220, 0], + ctm2_3: [130, 210, 0], + ctm1: [130, 210, 0], + }, + discreteRAuto: { + ctm0: [100, 200, Math.PI / 4], + ctm1_6: [100, 200, Math.PI / 4], + ctm1_3: [120, 220, -Math.PI / 4], + ctm2_3: [130, 210, -Math.PI / 4], + ctm1: [130, 210, -Math.PI / 4], + }, + justMoveBasic: { + ctm0: [40, 80, 0], + ctm1_6: [40, 80, 0], + ctm1_3: [40, 80, 0], + ctm2_3: [40, 80, 0], + ctm1: [40, 80, 0], + }, + justMoveR60: { + ctm0: [40, 80, Math.PI / 3], + ctm1_6: [40, 80, Math.PI / 3], + ctm1_3: [40, 80, Math.PI / 3], + ctm2_3: [40, 80, Math.PI / 3], + ctm1: [40, 80, Math.PI / 3], + }, + justMoveRAuto: { + ctm0: [40, 80, Math.atan(2)], + ctm1_6: [40, 80, Math.atan(2)], + ctm1_3: [40, 80, Math.atan(2)], + ctm2_3: [40, 80, Math.atan(2)], + ctm1: [40, 80, Math.atan(2)], + }, + justMoveRAutoReverse: { + ctm0: [40, 80, Math.PI + Math.atan(2)], + ctm1_6: [40, 80, Math.PI + Math.atan(2)], + ctm1_3: [40, 80, Math.PI + Math.atan(2)], + ctm2_3: [40, 80, Math.PI + Math.atan(2)], + ctm1: [40, 80, Math.PI + Math.atan(2)], + }, + nullMoveBasic: { + ctm0: [0, 0, 0], + ctm1_6: [0, 0, 0], + ctm1_3: [0, 0, 0], + ctm2_3: [0, 0, 0], + ctm1: [0, 0, 0], + }, + nullMoveRAutoReverse: { + ctm0: [0, 0, Math.PI], + ctm1_6: [0, 0, Math.PI], + ctm1_3: [0, 0, Math.PI], + ctm2_3: [0, 0, Math.PI], + ctm1: [0, 0, Math.PI], + }, +}; + +var gMotionBundles = [ + // Bundle to test basic functionality (using default calcMode='paced') + new TestcaseBundle(gMotionAttr, [ + // Basic paced-mode (default) test, with values/mpath/path + new AnimMotionTestcase( + { values: "100, 200; 120, 220; 130, 210" }, + _reusedCTMLists.pacedBasic + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210" }, + _reusedCTMLists.pacedBasic + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210" }, + _reusedCTMLists.pacedBasic + ), + + // ..and now with rotate=constant value in degrees + new AnimMotionTestcase( + { values: "100,200; 120,220; 130, 210", rotate: "60" }, + _reusedCTMLists.pacedR60 + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", rotate: "60" }, + _reusedCTMLists.pacedR60 + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210", rotate: "60" }, + _reusedCTMLists.pacedR60 + ), + + // ..and now with rotate=constant value in radians + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", rotate: "1.0471975512rad" }, // pi/3 + _reusedCTMLists.pacedR60 + ), + + // ..and now with rotate=auto + new AnimMotionTestcase( + { values: "100,200; 120,220; 130, 210", rotate: "auto" }, + _reusedCTMLists.pacedRAuto + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", rotate: "auto" }, + _reusedCTMLists.pacedRAuto + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210", rotate: "auto" }, + _reusedCTMLists.pacedRAuto + ), + + // ..and now with rotate=auto-reverse + new AnimMotionTestcase( + { values: "100,200; 120,220; 130, 210", rotate: "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", rotate: "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210", rotate: "auto-reverse" }, + _reusedCTMLists.pacedRAutoReverse + ), + ]), + + // Bundle to test calcMode='discrete' + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "100, 200; 120, 220; 130, 210", calcMode: "discrete" }, + _reusedCTMLists.discreteBasic + ), + new AnimMotionTestcase( + { path: "M100 200 L120 220 L130 210", calcMode: "discrete" }, + _reusedCTMLists.discreteBasic + ), + new AnimMotionTestcase( + { mpath: "M100 200 L120 220 L130 210", calcMode: "discrete" }, + _reusedCTMLists.discreteBasic + ), + // ..and now with rotate=auto + new AnimMotionTestcase( + { + values: "100, 200; 120, 220; 130, 210", + calcMode: "discrete", + rotate: "auto", + }, + _reusedCTMLists.discreteRAuto + ), + new AnimMotionTestcase( + { + path: "M100 200 L120 220 L130 210", + calcMode: "discrete", + rotate: "auto", + }, + _reusedCTMLists.discreteRAuto + ), + new AnimMotionTestcase( + { + mpath: "M100 200 L120 220 L130 210", + calcMode: "discrete", + rotate: "auto", + }, + _reusedCTMLists.discreteRAuto + ), + ]), + + // Bundle to test relative units ('em') + new TestcaseBundle(gMotionAttr, [ + // First with unitless values from->by... + new AnimMotionTestcase( + { from: "10, 10", by: "30, 60" }, + { + ctm0: [10, 10, 0], + ctm1_6: [15, 20, 0], + ctm1_3: [20, 30, 0], + ctm2_3: [30, 50, 0], + ctm1: [40, 70, 0], + } + ), + // ... then add 'em' units (with 1em=10px) on half the values + new AnimMotionTestcase( + { from: "1em, 10", by: "30, 6em" }, + { + ctm0: [10, 10, 0], + ctm1_6: [15, 20, 0], + ctm1_3: [20, 30, 0], + ctm2_3: [30, 50, 0], + ctm1: [40, 70, 0], + } + ), + ]), + + // Bundle to test a path with just a "move" command and nothing else + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase({ values: "40, 80" }, _reusedCTMLists.justMoveBasic), + new AnimMotionTestcase({ path: "M40 80" }, _reusedCTMLists.justMoveBasic), + new AnimMotionTestcase({ mpath: "m40 80" }, _reusedCTMLists.justMoveBasic), + ]), + // ... and now with a fixed rotate-angle + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "40, 80", rotate: "60" }, + _reusedCTMLists.justMoveR60 + ), + new AnimMotionTestcase( + { path: "M40 80", rotate: "60" }, + _reusedCTMLists.justMoveR60 + ), + new AnimMotionTestcase( + { mpath: "m40 80", rotate: "60" }, + _reusedCTMLists.justMoveR60 + ), + ]), + // ... and now with 'auto' (should use the move itself as + // our tangent angle, I think) + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "40, 80", rotate: "auto" }, + _reusedCTMLists.justMoveRAuto + ), + new AnimMotionTestcase( + { path: "M40 80", rotate: "auto" }, + _reusedCTMLists.justMoveRAuto + ), + new AnimMotionTestcase( + { mpath: "m40 80", rotate: "auto" }, + _reusedCTMLists.justMoveRAuto + ), + ]), + // ... and now with 'auto-reverse' + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "40, 80", rotate: "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse + ), + new AnimMotionTestcase( + { path: "M40 80", rotate: "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse + ), + new AnimMotionTestcase( + { mpath: "m40 80", rotate: "auto-reverse" }, + _reusedCTMLists.justMoveRAutoReverse + ), + ]), + // ... and now with a null move to make sure 'auto'/'auto-reverse' don't + // blow up + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "0, 0", rotate: "auto" }, + _reusedCTMLists.nullMoveBasic + ), + ]), + new TestcaseBundle(gMotionAttr, [ + new AnimMotionTestcase( + { values: "0, 0", rotate: "auto-reverse" }, + _reusedCTMLists.nullMoveRAutoReverse + ), + ]), +]; + +// XXXdholbert Add more tests: +// - keyPoints/keyTimes +// - paths with curves +// - Control path with from/by/to diff --git a/dom/smil/test/db_smilCSSFromBy.js b/dom/smil/test/db_smilCSSFromBy.js new file mode 100644 index 0000000000..737037271d --- /dev/null +++ b/dom/smil/test/db_smilCSSFromBy.js @@ -0,0 +1,207 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* testcase data for simple "from-by" animations of CSS properties */ + +// NOTE: This js file requires db_smilCSSPropertyList.js + +// Lists of testcases for re-use across multiple properties of the same type +var _fromByTestLists = { + color: [ + new AnimTestcaseFromBy("rgb(10, 20, 30)", "currentColor", { + midComp: "rgb(35, 45, 55)", + toComp: "rgb(60, 70, 80)", + }), + new AnimTestcaseFromBy("currentColor", "rgb(30, 20, 10)", { + fromComp: "rgb(50, 50, 50)", + midComp: "rgb(65, 60, 55)", + toComp: "rgb(80, 70, 60)", + }), + new AnimTestcaseFromBy( + "rgba(10, 20, 30, 0.2)", + "rgba(50, 50, 50, 1)", + // (rgb(10, 20, 30) * 0.2 * 0.5 + rgb(52, 54, 56) * 1.0 * 0.5) * (1 / 0.6) + { + midComp: "rgba(45, 48, 52, 0.6)", + // (rgb(10, 20, 30) * 0.2 + rgb(50, 50, 50) * 1) / 1.0 + toComp: "rgb(52, 54, 56)", + } + ), + + // The "from" and "by" values in the test case below overflow the maxium + // color-channel values when added together. + // (e.g. for red [ignoring alpha for now], 100 + 240 = 340 which is > 255) + // + // The SVG Animation spec says we should clamp color values "as late as + // possible" i.e. allow the channel overflow and clamp at paint-time. + // + // That gives us: + // + // to-value = (rgb(100, 100, 100) * 0.6 + rgb(240, 240, 240) * 1.0)) * 1 + // = rgb(300, 300, 300) + // midComp = (rgb(100, 100, 100) * 0.6 * 0.5 + rgb(300, 300, 300) * 1.0 * 0.5) * (1 / 0.8) + // = rgb(225, 225, 225) + // + // + new AnimTestcaseFromBy( + "rgba(100, 100, 100, 0.6)", + "rgba(240, 240, 240, 1)", + { midComp: "rgba(225, 225, 225, 0.8)", toComp: "rgb(255, 255, 255)" } + ), + ], + lengthNoUnits: [ + new AnimTestcaseFromBy("0", "50", { + fromComp: "0px", // 0 acts like 0px + midComp: "25px", + toComp: "50px", + }), + new AnimTestcaseFromBy("30", "10", { + fromComp: "30px", + midComp: "35px", + toComp: "40px", + }), + ], + lengthPx: [ + new AnimTestcaseFromBy("0px", "8px", { + fromComp: "0px", + midComp: "4px", + toComp: "8px", + }), + new AnimTestcaseFromBy("1px", "10px", { + fromComp: "1px", + midComp: "6px", + toComp: "11px", + }), + ], + opacity: [ + new AnimTestcaseFromBy("1", "-1", { midComp: "0.5", toComp: "0" }), + new AnimTestcaseFromBy("0.4", "-0.6", { midComp: "0.1", toComp: "0" }), + new AnimTestcaseFromBy( + "0.8", + "-1.4", + { midComp: "0.1", toComp: "0" }, + "opacities with abs val >1 get clamped too early" + ), + new AnimTestcaseFromBy( + "1.2", + "-0.6", + { midComp: "0.9", toComp: "0.6" }, + "opacities with abs val >1 get clamped too early" + ), + ], + paint: [ + // The "none" keyword & URI values aren't addiditve, so the animations in + // these testcases are expected to have no effect. + new AnimTestcaseFromBy("none", "none", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "url(#gradB)", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "url(#gradB) red", { noEffect: 1 }), + new AnimTestcaseFromBy("url(#gradA)", "none", { noEffect: 1 }), + new AnimTestcaseFromBy("red", "url(#gradA)", { noEffect: 1 }), + ], + URIsAndNone: [ + // No need to specify { noEffect: 1 }, since plain URI-valued properties + // aren't additive + new AnimTestcaseFromBy("url(#idA)", "url(#idB)"), + new AnimTestcaseFromBy("none", "url(#idB)"), + new AnimTestcaseFromBy("url(#idB)", "inherit"), + ], +}; + +// List of attribute/testcase-list bundles to be tested +var gFromByBundles = [ + new TestcaseBundle(gPropList.clip, [ + new AnimTestcaseFromBy( + "rect(1px, 2px, 3px, 4px)", + "rect(10px, 20px, 30px, 40px)", + { + midComp: "rect(6px, 12px, 18px, 24px)", + toComp: "rect(11px, 22px, 33px, 44px)", + } + ), + // Adding "auto" (either as a standalone value or a subcomponent value) + // should cause animation to fail. + new AnimTestcaseFromBy("auto", "auto", { noEffect: 1 }), + new AnimTestcaseFromBy("auto", "rect(auto, auto, auto, auto)", { + noEffect: 1, + }), + new AnimTestcaseFromBy( + "rect(auto, auto, auto, auto)", + "rect(auto, auto, auto, auto)", + { noEffect: 1 } + ), + new AnimTestcaseFromBy("rect(1px, 2px, 3px, 4px)", "auto", { noEffect: 1 }), + new AnimTestcaseFromBy("auto", "rect(1px, 2px, 3px, 4px)", { noEffect: 1 }), + new AnimTestcaseFromBy( + "rect(1px, 2px, 3px, auto)", + "rect(10px, 20px, 30px, 40px)", + { noEffect: 1 } + ), + new AnimTestcaseFromBy( + "rect(1px, auto, 3px, 4px)", + "rect(10px, auto, 30px, 40px)", + { noEffect: 1 } + ), + new AnimTestcaseFromBy( + "rect(1px, 2px, 3px, 4px)", + "rect(10px, auto, 30px, 40px)", + { noEffect: 1 } + ), + ]), + // Check that 'by' animations for 'cursor' has no effect + new TestcaseBundle(gPropList.cursor, [ + new AnimTestcaseFromBy("crosshair", "move"), + ]), + new TestcaseBundle( + gPropList.fill, + [].concat(_fromByTestLists.color, _fromByTestLists.paint) + ), + // Check that 'by' animations involving URIs have no effect + new TestcaseBundle(gPropList.filter, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.font, [ + new AnimTestcaseFromBy( + "10px serif", + "normal normal 400 100px / 10px monospace" + ), + ]), + new TestcaseBundle( + gPropList.font_size, + [].concat(_fromByTestLists.lengthNoUnits, _fromByTestLists.lengthPx) + ), + new TestcaseBundle(gPropList.font_size_adjust, [ + // These testcases implicitly have no effect, because font-size-adjust is + // non-additive (and is declared as such in db_smilCSSPropertyList.js) + new AnimTestcaseFromBy("0.5", "0.1"), + new AnimTestcaseFromBy("none", "0.1"), + new AnimTestcaseFromBy("0.1", "none"), + ]), + // Bug 1457353: Change from nsColor to StyleComplexColor causes addition + // with currentcolor to break. Bug 1465307 for work to re-enable. + new TestcaseBundle(gPropList.lighting_color, _fromByTestLists.color), + new TestcaseBundle(gPropList.marker, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_end, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_mid, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_start, _fromByTestLists.URIsAndNone), + new TestcaseBundle(gPropList.overflow, [ + new AnimTestcaseFromBy("inherit", "auto"), + new AnimTestcaseFromBy("scroll", "hidden"), + ]), + new TestcaseBundle(gPropList.opacity, _fromByTestLists.opacity), + new TestcaseBundle(gPropList.stroke_miterlimit, [ + new AnimTestcaseFromBy("1", "1", { midComp: "1.5", toComp: "2" }), + new AnimTestcaseFromBy("20.1", "-10", { midComp: "15.1", toComp: "10.1" }), + ]), + new TestcaseBundle(gPropList.stroke_dasharray, [ + // These testcases implicitly have no effect, because stroke-dasharray is + // non-additive (and is declared as such in db_smilCSSPropertyList.js) + new AnimTestcaseFromBy("none", "5"), + new AnimTestcaseFromBy("10", "5"), + new AnimTestcaseFromBy("1", "2, 3"), + ]), + new TestcaseBundle( + gPropList.stroke_width, + [].concat(_fromByTestLists.lengthNoUnits, _fromByTestLists.lengthPx) + ), +]; diff --git a/dom/smil/test/db_smilCSSFromTo.js b/dom/smil/test/db_smilCSSFromTo.js new file mode 100644 index 0000000000..e33c38819c --- /dev/null +++ b/dom/smil/test/db_smilCSSFromTo.js @@ -0,0 +1,625 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* testcase data for simple "from-to" animations of CSS properties */ + +// NOTE: This js file requires db_smilCSSPropertyList.js + +// NOTE: I'm Including 'inherit' and 'currentColor' as interpolatable values. +// According to SVG Mobile 1.2 section 16.2.9, "keywords such as inherit which +// yield a numeric computed value may be included in the values list for an +// interpolated animation". + +// Path of test URL (stripping off final slash + filename), for use in +// generating computed value of 'cursor' property +var _testPath = document.URL.substring(0, document.URL.lastIndexOf("/")); + +// Lists of testcases for re-use across multiple properties of the same type +var _fromToTestLists = { + color: [ + new AnimTestcaseFromTo("rgb(100, 100, 100)", "rgb(200, 200, 200)", { + midComp: "rgb(150, 150, 150)", + }), + new AnimTestcaseFromTo("#F02000", "#0080A0", { + fromComp: "rgb(240, 32, 0)", + midComp: "rgb(120, 80, 80)", + toComp: "rgb(0, 128, 160)", + }), + new AnimTestcaseFromTo("crimson", "lawngreen", { + fromComp: "rgb(220, 20, 60)", + midComp: "rgb(172, 136, 30)", + toComp: "rgb(124, 252, 0)", + }), + new AnimTestcaseFromTo("currentColor", "rgb(100, 100, 100)", { + fromComp: "rgb(50, 50, 50)", + midComp: "rgb(75, 75, 75)", + }), + new AnimTestcaseFromTo( + "rgba(10, 20, 30, 0.2)", + "rgba(50, 50, 50, 1)", + // (rgb(10, 20, 30) * 0.2 * 0.5 + rgb(50, 50, 50) * 1.0 * 0.5) * (1 / 0.6) + { midComp: "rgba(43, 45, 47, 0.6)", toComp: "rgb(50, 50, 50)" } + ), + ], + colorFromInheritBlack: [ + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", { + fromComp: "rgb(0, 0, 0)", + midComp: "rgb(100, 100, 100)", + }), + ], + colorFromInheritWhite: [ + new AnimTestcaseFromTo("inherit", "rgb(205, 205, 205)", { + fromComp: "rgb(255, 255, 255)", + midComp: "rgb(230, 230, 230)", + }), + ], + paintServer: [ + new AnimTestcaseFromTo("none", "none"), + new AnimTestcaseFromTo("none", "blue", { toComp: "rgb(0, 0, 255)" }), + new AnimTestcaseFromTo("rgb(50, 50, 50)", "none"), + new AnimTestcaseFromTo( + "url(#gradA)", + "url(#gradB) currentColor", + { + fromComp: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + toComp: 'url("' + document.URL + '#gradB") rgb(50, 50, 50)', + }, + "need support for URI-based paints" + ), + new AnimTestcaseFromTo( + "url(#gradA) orange", + "url(#gradB)", + { + fromComp: 'url("' + document.URL + '#gradA") rgb(255, 165, 0)', + toComp: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + }, + "need support for URI-based paints" + ), + new AnimTestcaseFromTo( + "url(#no_grad)", + "url(#gradB)", + { + fromComp: 'url("' + document.URL + '#no_grad") ' + "rgb(0, 0, 0)", + toComp: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + }, + "need support for URI-based paints" + ), + new AnimTestcaseFromTo( + "url(#no_grad) rgb(1,2,3)", + "url(#gradB) blue", + { + fromComp: 'url("' + document.URL + '#no_grad") ' + "rgb(1, 2, 3)", + toComp: 'url("' + document.URL + '#gradB") rgb(0, 0, 255)', + }, + "need support for URI-based paints" + ), + ], + lengthNoUnits: [ + new AnimTestcaseFromTo("0", "20", { + fromComp: "0px", + midComp: "10px", + toComp: "20px", + }), + new AnimTestcaseFromTo("50", "0", { + fromComp: "50px", + midComp: "25px", + toComp: "0px", + }), + new AnimTestcaseFromTo("30", "80", { + fromComp: "30px", + midComp: "55px", + toComp: "80px", + }), + ], + lengthPx: [ + new AnimTestcaseFromTo("0px", "12px", { + fromComp: "0px", + midComp: "6px", + toComp: "12px", + }), + new AnimTestcaseFromTo("16px", "0px", { + fromComp: "16px", + midComp: "8px", + toComp: "0px", + }), + new AnimTestcaseFromTo("10px", "20px", { + fromComp: "10px", + midComp: "15px", + toComp: "20px", + }), + new AnimTestcaseFromTo("41px", "1px", { + fromComp: "41px", + midComp: "21px", + toComp: "1px", + }), + ], + lengthPctSVG: [new AnimTestcaseFromTo("20.5%", "0.5%", { midComp: "10.5%" })], + lengthPxPctSVG: [ + new AnimTestcaseFromTo( + "10px", + "10%", + { midComp: "15px" }, + "need support for interpolating between " + "px and percent values" + ), + ], + lengthPxNoUnitsSVG: [ + new AnimTestcaseFromTo("10", "20px", { + fromComp: "10px", + midComp: "15px", + toComp: "20px", + }), + new AnimTestcaseFromTo("10px", "20", { + fromComp: "10px", + midComp: "15px", + toComp: "20px", + }), + ], + opacity: [ + new AnimTestcaseFromTo("1", "0", { midComp: "0.5" }), + new AnimTestcaseFromTo("0.2", "0.12", { midComp: "0.16" }), + new AnimTestcaseFromTo("0.5", "0.7", { midComp: "0.6" }), + new AnimTestcaseFromTo("0.5", "inherit", { midComp: "0.75", toComp: "1" }), + // Make sure we don't clamp out-of-range values before interpolation + new AnimTestcaseFromTo( + "0.2", + "1.2", + { midComp: "0.7", toComp: "1" }, + "opacities with abs val >1 get clamped too early" + ), + new AnimTestcaseFromTo("-0.2", "0.6", { fromComp: "0", midComp: "0.2" }), + new AnimTestcaseFromTo( + "-1.2", + "1.6", + { fromComp: "0", midComp: "0.2", toComp: "1" }, + "opacities with abs val >1 get clamped too early" + ), + new AnimTestcaseFromTo( + "-0.6", + "1.4", + { fromComp: "0", midComp: "0.4", toComp: "1" }, + "opacities with abs val >1 get clamped too early" + ), + ], + URIsAndNone: [ + new AnimTestcaseFromTo("url(#idA)", "url(#idB)", { + fromComp: 'url("#idA")', + toComp: 'url("#idB")', + }), + new AnimTestcaseFromTo("none", "url(#idB)", { toComp: 'url("#idB")' }), + new AnimTestcaseFromTo("url(#idB)", "inherit", { + fromComp: 'url("#idB")', + toComp: "none", + }), + ], +}; + +function _tweakForLetterSpacing(testcases) { + return testcases.map(function(t) { + let valMap = Object.assign({}, t.computedValMap); + for (let prop of Object.keys(valMap)) { + if (valMap[prop] == "0px") { + valMap[prop] = "normal"; + } + } + return new AnimTestcaseFromTo(t.from, t.to, valMap); + }); +} + +// List of attribute/testcase-list bundles to be tested +var gFromToBundles = [ + new TestcaseBundle(gPropList.clip, [ + new AnimTestcaseFromTo( + "rect(1px, 2px, 3px, 4px)", + "rect(11px, 22px, 33px, 44px)", + { midComp: "rect(6px, 12px, 18px, 24px)" } + ), + new AnimTestcaseFromTo( + "rect(1px, auto, 3px, 4px)", + "rect(11px, auto, 33px, 44px)", + { midComp: "rect(6px, auto, 18px, 24px)" } + ), + new AnimTestcaseFromTo("auto", "auto"), + new AnimTestcaseFromTo( + "rect(auto, auto, auto, auto)", + "rect(auto, auto, auto, auto)" + ), + // Interpolation not supported in these next cases (with auto --> px-value) + new AnimTestcaseFromTo( + "rect(1px, auto, 3px, auto)", + "rect(11px, auto, 33px, 44px)" + ), + new AnimTestcaseFromTo( + "rect(1px, 2px, 3px, 4px)", + "rect(11px, auto, 33px, 44px)" + ), + new AnimTestcaseFromTo("rect(1px, 2px, 3px, 4px)", "auto"), + new AnimTestcaseFromTo("auto", "rect(1px, 2px, 3px, 4px)"), + ]), + new TestcaseBundle(gPropList.clip_path, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.clip_rule, [ + new AnimTestcaseFromTo("nonzero", "evenodd"), + new AnimTestcaseFromTo("evenodd", "inherit", { toComp: "nonzero" }), + ]), + new TestcaseBundle( + gPropList.color, + [].concat(_fromToTestLists.color, [ + // Note: inherited value is rgb(50, 50, 50) (set on <svg>) + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", { + fromComp: "rgb(50, 50, 50)", + midComp: "rgb(125, 125, 125)", + }), + ]) + ), + new TestcaseBundle(gPropList.color_interpolation, [ + new AnimTestcaseFromTo("sRGB", "auto", { fromComp: "srgb" }), + new AnimTestcaseFromTo("inherit", "linearRGB", { + fromComp: "srgb", + toComp: "linearrgb", + }), + ]), + new TestcaseBundle(gPropList.color_interpolation_filters, [ + new AnimTestcaseFromTo("sRGB", "auto", { fromComp: "srgb" }), + new AnimTestcaseFromTo("auto", "inherit", { toComp: "linearrgb" }), + ]), + new TestcaseBundle(gPropList.cursor, [ + new AnimTestcaseFromTo("crosshair", "move"), + new AnimTestcaseFromTo( + "url('a.cur'), url('b.cur'), nw-resize", + "sw-resize", + { + fromComp: + 'url("' + + _testPath + + '/a.cur"), ' + + 'url("' + + _testPath + + '/b.cur"), ' + + "nw-resize", + } + ), + ]), + new TestcaseBundle(gPropList.direction, [ + new AnimTestcaseFromTo("ltr", "rtl"), + new AnimTestcaseFromTo("rtl", "inherit"), + ]), + new TestcaseBundle(gPropList.display, [ + // I'm not testing the "inherit" value for "display", because part of + // my test runs with "display: none" on everything, and so the + // inherited value isn't always the same. (i.e. the computed value + // of 'inherit' will be different in different tests) + new AnimTestcaseFromTo("block", "table-cell"), + new AnimTestcaseFromTo("inline", "inline-table"), + new AnimTestcaseFromTo("table-row", "none"), + ]), + new TestcaseBundle(gPropList.dominant_baseline, [ + new AnimTestcaseFromTo("alphabetic", "hanging"), + new AnimTestcaseFromTo("mathematical", "central"), + new AnimTestcaseFromTo("middle", "text-after-edge"), + new AnimTestcaseFromTo("text-before-edge", "auto"), + new AnimTestcaseFromTo("alphabetic", "inherit", { toComp: "auto" }), + ]), + // NOTE: Mozilla doesn't currently support "enable-background", but I'm + // testing it here in case we ever add support for it, because it's + // explicitly not animatable in the SVG spec. + new TestcaseBundle(gPropList.enable_background, [ + new AnimTestcaseFromTo("new", "accumulate"), + ]), + new TestcaseBundle( + gPropList.fill, + [].concat( + _fromToTestLists.color, + _fromToTestLists.paintServer, + _fromToTestLists.colorFromInheritBlack + ) + ), + new TestcaseBundle(gPropList.fill_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.fill_rule, [ + new AnimTestcaseFromTo("nonzero", "evenodd"), + new AnimTestcaseFromTo("evenodd", "inherit", { toComp: "nonzero" }), + ]), + new TestcaseBundle(gPropList.filter, _fromToTestLists.URIsAndNone), + new TestcaseBundle( + gPropList.flood_color, + [].concat(_fromToTestLists.color, _fromToTestLists.colorFromInheritBlack) + ), + new TestcaseBundle(gPropList.flood_opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.font, [ + // NOTE: 'line-height' is hard-wired at 10px in test_smilCSSFromTo.xhtml + // because if it's not explicitly set, its value varies across platforms. + // NOTE: System font values can't be tested here, because their computed + // values vary from platform to platform. However, they are tested + // visually, in the reftest "anim-css-font-1.svg" + new AnimTestcaseFromTo("10px serif", "30px serif", { + fromComp: "normal normal 400 10px / 10px serif", + toComp: "normal normal 400 30px / 10px serif", + }), + new AnimTestcaseFromTo("10px serif", "30px sans-serif", { + fromComp: "normal normal 400 10px / 10px serif", + toComp: "normal normal 400 30px / 10px sans-serif", + }), + new AnimTestcaseFromTo("1px / 90px cursive", "100px monospace", { + fromComp: "normal normal 400 1px / 10px cursive", + toComp: "normal normal 400 100px / 10px monospace", + }), + new AnimTestcaseFromTo( + "italic small-caps 200 1px cursive", + "100px monospace", + { + fromComp: "italic small-caps 200 1px / 10px cursive", + toComp: "normal normal 400 100px / 10px monospace", + } + ), + new AnimTestcaseFromTo( + "oblique normal 200 30px / 10px cursive", + "normal small-caps 800 40px / 10px serif" + ), + ]), + new TestcaseBundle(gPropList.font_family, [ + new AnimTestcaseFromTo("serif", "sans-serif"), + new AnimTestcaseFromTo("cursive", "monospace"), + ]), + new TestcaseBundle( + gPropList.font_size, + [].concat(_fromToTestLists.lengthNoUnits, _fromToTestLists.lengthPx, [ + new AnimTestcaseFromTo("10px", "40%", { + midComp: "15px", + toComp: "20px", + }), + new AnimTestcaseFromTo("160%", "80%", { + fromComp: "80px", + midComp: "60px", + toComp: "40px", + }), + ]) + ), + new TestcaseBundle(gPropList.font_size_adjust, [ + new AnimTestcaseFromTo("0.9", "0.1", { midComp: "0.5" }), + new AnimTestcaseFromTo("0.5", "0.6", { midComp: "0.55" }), + new AnimTestcaseFromTo("none", "0.4"), + ]), + new TestcaseBundle(gPropList.font_stretch, [ + new AnimTestcaseFromTo( + "normal", + "wider", + {}, + "need support for animating between " + "relative 'font-stretch' values" + ), + new AnimTestcaseFromTo( + "narrower", + "ultra-condensed", + {}, + "need support for animating between " + "relative 'font-stretch' values" + ), + new AnimTestcaseFromTo("ultra-condensed", "condensed", { + fromComp: "50%", + midComp: "62.5%", + toComp: "75%", + }), + new AnimTestcaseFromTo("semi-condensed", "semi-expanded", { + fromComp: "87.5%", + midComp: "100%", + toComp: "112.5%", + }), + new AnimTestcaseFromTo("expanded", "ultra-expanded", { + fromComp: "125%", + midComp: "162.5%", + toComp: "200%", + }), + new AnimTestcaseFromTo("ultra-expanded", "inherit", { + fromComp: "200%", + midComp: "150%", + toComp: "100%", + }), + ]), + new TestcaseBundle(gPropList.font_style, [ + new AnimTestcaseFromTo("italic", "inherit", { toComp: "normal" }), + new AnimTestcaseFromTo("normal", "italic"), + new AnimTestcaseFromTo("italic", "oblique"), + new AnimTestcaseFromTo("oblique", "normal", { midComp: "oblique 7deg" }), + ]), + new TestcaseBundle(gPropList.font_variant, [ + new AnimTestcaseFromTo("inherit", "small-caps", { fromComp: "normal" }), + new AnimTestcaseFromTo("small-caps", "normal"), + ]), + new TestcaseBundle(gPropList.font_weight, [ + new AnimTestcaseFromTo("100", "900", { midComp: "500" }), + new AnimTestcaseFromTo("700", "100", { midComp: "400" }), + new AnimTestcaseFromTo("inherit", "200", { + fromComp: "400", + midComp: "300", + }), + new AnimTestcaseFromTo("normal", "bold", { + fromComp: "400", + midComp: "550", + toComp: "700", + }), + new AnimTestcaseFromTo( + "lighter", + "bolder", + {}, + "need support for animating between " + "relative 'font-weight' values" + ), + ]), + // NOTE: Mozilla doesn't currently support "glyph-orientation-horizontal" or + // "glyph-orientation-vertical", but I'm testing them here in case we ever + // add support for them, because they're explicitly not animatable in the SVG + // spec. + new TestcaseBundle(gPropList.glyph_orientation_horizontal, [ + new AnimTestcaseFromTo("45deg", "60deg"), + ]), + new TestcaseBundle(gPropList.glyph_orientation_vertical, [ + new AnimTestcaseFromTo("45deg", "60deg"), + ]), + new TestcaseBundle(gPropList.image_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeQuality", { + toComp: "optimizequality", + }), + new AnimTestcaseFromTo("optimizeQuality", "optimizeSpeed", { + fromComp: "optimizequality", + toComp: "optimizespeed", + }), + ]), + new TestcaseBundle( + gPropList.letter_spacing, + _tweakForLetterSpacing( + [].concat(_fromToTestLists.lengthNoUnits, _fromToTestLists.lengthPx) + ) + ), + new TestcaseBundle( + gPropList.lighting_color, + [].concat(_fromToTestLists.color, _fromToTestLists.colorFromInheritWhite) + ), + new TestcaseBundle(gPropList.marker, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_end, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_mid, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.marker_start, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.mask, _fromToTestLists.URIsAndNone), + new TestcaseBundle(gPropList.opacity, _fromToTestLists.opacity), + new TestcaseBundle(gPropList.overflow, [ + new AnimTestcaseFromTo("auto", "visible"), + new AnimTestcaseFromTo("inherit", "visible", { fromComp: "hidden" }), + new AnimTestcaseFromTo("scroll", "auto"), + ]), + new TestcaseBundle(gPropList.pointer_events, [ + new AnimTestcaseFromTo("visibleFill", "stroke", { + fromComp: "visiblefill", + }), + new AnimTestcaseFromTo("none", "visibleStroke", { + toComp: "visiblestroke", + }), + ]), + new TestcaseBundle(gPropList.shape_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeSpeed", { + toComp: "optimizespeed", + }), + new AnimTestcaseFromTo("crispEdges", "geometricPrecision", { + fromComp: "crispedges", + toComp: "geometricprecision", + }), + ]), + new TestcaseBundle( + gPropList.stop_color, + [].concat(_fromToTestLists.color, _fromToTestLists.colorFromInheritBlack) + ), + new TestcaseBundle(gPropList.stop_opacity, _fromToTestLists.opacity), + new TestcaseBundle( + gPropList.stroke, + [].concat(_fromToTestLists.color, _fromToTestLists.paintServer, [ + // Note: inherited value is "none" (the default for "stroke" property) + new AnimTestcaseFromTo("inherit", "rgb(200, 200, 200)", { + fromComp: "none", + }), + ]) + ), + new TestcaseBundle( + gPropList.stroke_dasharray, + [].concat(_fromToTestLists.lengthPctSVG, [ + new AnimTestcaseFromTo("inherit", "20", { fromComp: "none" }), + new AnimTestcaseFromTo("1", "none"), + new AnimTestcaseFromTo("10", "20", { midComp: "15" }), + new AnimTestcaseFromTo("1", "2, 3", { + fromComp: "1, 1", + midComp: "1.5, 2", + }), + new AnimTestcaseFromTo("2, 8", "6", { midComp: "4, 7" }), + new AnimTestcaseFromTo("1, 3", "1, 3, 5, 7, 9", { + fromComp: "1, 3, 1, 3, 1, 3, 1, 3, 1, 3", + midComp: "1, 3, 3, 5, 5, 2, 2, 4, 4, 6", + }), + ]) + ), + new TestcaseBundle( + gPropList.stroke_dashoffset, + [].concat( + _fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG, + _fromToTestLists.lengthPctSVG, + _fromToTestLists.lengthPxNoUnitsSVG + ) + ), + new TestcaseBundle(gPropList.stroke_linecap, [ + new AnimTestcaseFromTo("butt", "round"), + new AnimTestcaseFromTo("round", "square"), + ]), + new TestcaseBundle(gPropList.stroke_linejoin, [ + new AnimTestcaseFromTo("miter", "round"), + new AnimTestcaseFromTo("round", "bevel"), + ]), + new TestcaseBundle(gPropList.stroke_miterlimit, [ + new AnimTestcaseFromTo("1", "2", { midComp: "1.5" }), + new AnimTestcaseFromTo("20.1", "10.1", { midComp: "15.1" }), + ]), + new TestcaseBundle(gPropList.stroke_opacity, _fromToTestLists.opacity), + new TestcaseBundle( + gPropList.stroke_width, + [].concat( + _fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG, + _fromToTestLists.lengthPctSVG, + _fromToTestLists.lengthPxNoUnitsSVG, + [ + new AnimTestcaseFromTo("inherit", "7px", { + fromComp: "1px", + midComp: "4px", + toComp: "7px", + }), + ] + ) + ), + new TestcaseBundle(gPropList.text_anchor, [ + new AnimTestcaseFromTo("start", "middle"), + new AnimTestcaseFromTo("middle", "end"), + ]), + new TestcaseBundle(gPropList.text_decoration_line, [ + new AnimTestcaseFromTo("none", "underline"), + new AnimTestcaseFromTo("overline", "line-through"), + new AnimTestcaseFromTo("blink", "underline"), + ]), + new TestcaseBundle(gPropList.text_rendering, [ + new AnimTestcaseFromTo("auto", "optimizeSpeed", { + toComp: "optimizespeed", + }), + new AnimTestcaseFromTo("optimizeSpeed", "geometricPrecision", { + fromComp: "optimizespeed", + toComp: "geometricprecision", + }), + new AnimTestcaseFromTo("geometricPrecision", "optimizeLegibility", { + fromComp: "geometricprecision", + toComp: "optimizelegibility", + }), + ]), + new TestcaseBundle(gPropList.unicode_bidi, [ + new AnimTestcaseFromTo("embed", "bidi-override"), + ]), + new TestcaseBundle(gPropList.vector_effect, [ + new AnimTestcaseFromTo("none", "non-scaling-stroke"), + ]), + new TestcaseBundle(gPropList.visibility, [ + new AnimTestcaseFromTo("visible", "hidden"), + new AnimTestcaseFromTo("hidden", "collapse"), + ]), + new TestcaseBundle( + gPropList.word_spacing, + [].concat( + _fromToTestLists.lengthNoUnits, + _fromToTestLists.lengthPx, + _fromToTestLists.lengthPxPctSVG + ) + ), + new TestcaseBundle( + gPropList.word_spacing, + _fromToTestLists.lengthPctSVG, + "pct->pct animations don't currently work for " + "*-spacing properties" + ), + // NOTE: Mozilla doesn't currently support "writing-mode", but I'm + // testing it here in case we ever add support for it, because it's + // explicitly not animatable in the SVG spec. + new TestcaseBundle(gPropList.writing_mode, [ + new AnimTestcaseFromTo("lr", "rl"), + ]), +]; diff --git a/dom/smil/test/db_smilCSSPaced.js b/dom/smil/test/db_smilCSSPaced.js new file mode 100644 index 0000000000..48fbe9a154 --- /dev/null +++ b/dom/smil/test/db_smilCSSPaced.js @@ -0,0 +1,356 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 4 -*- */ +/* vim: set shiftwidth=4 tabstop=4 autoindent cindent noexpandtab: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* testcase data for paced-mode animations of CSS properties */ + +// Lists of testcases for re-use across multiple properties of the same type +var _pacedTestLists = { + color: [ + new AnimTestcasePaced( + "rgb(2, 4, 6); " + "rgb(4, 8, 12); " + "rgb(8, 16, 24)", + { + comp0: "rgb(2, 4, 6)", + comp1_6: "rgb(3, 6, 9)", + comp1_3: "rgb(4, 8, 12)", + comp2_3: "rgb(6, 12, 18)", + comp1: "rgb(8, 16, 24)", + } + ), + new AnimTestcasePaced( + "rgb(10, 10, 10); " + "rgb(20, 10, 8); " + "rgb(20, 30, 4)", + { + comp0: "rgb(10, 10, 10)", + comp1_6: "rgb(15, 10, 9)", + comp1_3: "rgb(20, 10, 8)", + comp2_3: "rgb(20, 20, 6)", + comp1: "rgb(20, 30, 4)", + } + ), + // Use the same RGB component values to make + // premultication effect easier to compute. + new AnimTestcasePaced( + "rgba(20, 40, 60, 0.2); " + + "rgba(20, 40, 60, 0.4); " + + "rgba(20, 40, 60, 0.8)", + { + comp0: "rgba(20, 40, 60, 0.2)", + comp1_6: "rgba(20, 40, 60, 0.3)", + comp1_3: "rgba(20, 40, 60, 0.4)", + comp2_3: "rgba(20, 40, 60, 0.6)", + comp1: "rgba(20, 40, 60, 0.8)", + } + ), + ], + currentColor_color: [ + new AnimTestcasePaced( + "olive; " + // rgb(128, 128, 0) + "currentColor; " + // rgb(50, 50, 50) + "rgb(206, 150, 206)", + { + comp0: "rgb(128, 128, 0)", + comp1_6: "rgb(89, 89, 25)", + comp1_3: "rgb(50, 50, 50)", + comp2_3: "rgb(128, 100, 128)", + comp1: "rgb(206, 150, 206)", + } + ), + ], + currentColor_fill: [ + // Bug 1467622 changed the distance calculation + // involving currentColor, comp values below + // are no longer evenly spaced. + new AnimTestcasePaced( + "olive; " + // rgb(128, 128, 0) + "currentColor; " + // rgb(50, 50, 50) + "rgb(206, 150, 206)", + { + comp0: "rgb(128, 128, 0)", + comp1_6: "rgb(98, 98, 19)", + comp1_3: "rgb(67, 67, 39)", + comp2_3: "rgb(115, 92, 115)", + comp1: "rgb(206, 150, 206)", + } + ), + ], + paintServer: [ + // Sanity check: These aren't interpolatable -- they should end up + // ignoring the calcMode="paced" and falling into discrete-mode. + new AnimTestcasePaced( + "url(#gradA); url(#gradB)", + { + comp0: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp1_6: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp1_3: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp2_3: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + comp1: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + }, + "need support for URI-based paints" + ), + new AnimTestcasePaced( + "url(#gradA); url(#gradB); url(#gradC)", + { + comp0: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp1_6: 'url("' + document.URL + '#gradA") rgb(0, 0, 0)', + comp1_3: 'url("' + document.URL + '#gradB") rgb(0, 0, 0)', + comp2_3: 'url("' + document.URL + '#gradC") rgb(0, 0, 0)', + comp1: 'url("' + document.URL + '#gradC") rgb(0, 0, 0)', + }, + "need support for URI-based paints" + ), + ], + lengthNoUnits: [ + new AnimTestcasePaced("2; 0; 4", { + comp0: "2px", + comp1_6: "1px", + comp1_3: "0px", + comp2_3: "2px", + comp1: "4px", + }), + new AnimTestcasePaced("10; 12; 8", { + comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px", + }), + ], + lengthPx: [ + new AnimTestcasePaced("0px; 2px; 6px", { + comp0: "0px", + comp1_6: "1px", + comp1_3: "2px", + comp2_3: "4px", + comp1: "6px", + }), + new AnimTestcasePaced("10px; 12px; 8px", { + comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px", + }), + ], + lengthPctSVG: [ + new AnimTestcasePaced("5%; 6%; 4%", { + comp0: "5%", + comp1_6: "5.5%", + comp1_3: "6%", + comp2_3: "5%", + comp1: "4%", + }), + ], + lengthPxPctSVG: [ + new AnimTestcasePaced( + "0px; 1%; 6px", + { + comp0: "0px", + comp1_6: "1px", + comp1_3: "1%", + comp2_3: "4px", + comp1: "6px", + }, + "need support for interpolating between " + "px and percent values" + ), + ], + opacity: [ + new AnimTestcasePaced("0; 0.2; 0.6", { + comp0: "0", + comp1_6: "0.1", + comp1_3: "0.2", + comp2_3: "0.4", + comp1: "0.6", + }), + new AnimTestcasePaced("0.7; 1.0; 0.4", { + comp0: "0.7", + comp1_6: "0.85", + comp1_3: "1", + comp2_3: "0.7", + comp1: "0.4", + }), + ], + rect: [ + new AnimTestcasePaced( + "rect(2px, 4px, 6px, 8px); " + + "rect(4px, 8px, 12px, 16px); " + + "rect(8px, 16px, 24px, 32px)", + { + comp0: "rect(2px, 4px, 6px, 8px)", + comp1_6: "rect(3px, 6px, 9px, 12px)", + comp1_3: "rect(4px, 8px, 12px, 16px)", + comp2_3: "rect(6px, 12px, 18px, 24px)", + comp1: "rect(8px, 16px, 24px, 32px)", + } + ), + new AnimTestcasePaced( + "rect(10px, 10px, 10px, 10px); " + + "rect(20px, 10px, 50px, 8px); " + + "rect(20px, 30px, 130px, 4px)", + { + comp0: "rect(10px, 10px, 10px, 10px)", + comp1_6: "rect(15px, 10px, 30px, 9px)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "rect(20px, 20px, 90px, 6px)", + comp1: "rect(20px, 30px, 130px, 4px)", + } + ), + new AnimTestcasePaced( + "rect(10px, auto, 10px, 10px); " + + "rect(20px, auto, 50px, 8px); " + + "rect(40px, auto, 130px, 4px)", + { + comp0: "rect(10px, auto, 10px, 10px)", + comp1_6: "rect(15px, auto, 30px, 9px)", + comp1_3: "rect(20px, auto, 50px, 8px)", + comp2_3: "rect(30px, auto, 90px, 6px)", + comp1: "rect(40px, auto, 130px, 4px)", + } + ), + // Paced-mode animation is not supported in these next few cases + // (Can't compute subcomponent distance between 'auto' & px-values) + new AnimTestcasePaced( + "rect(10px, 10px, 10px, auto); " + + "rect(20px, 10px, 50px, 8px); " + + "rect(20px, 30px, 130px, 4px)", + { + comp0: "rect(10px, 10px, 10px, auto)", + comp1_6: "rect(10px, 10px, 10px, auto)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "rect(20px, 30px, 130px, 4px)", + comp1: "rect(20px, 30px, 130px, 4px)", + } + ), + new AnimTestcasePaced( + "rect(10px, 10px, 10px, 10px); " + + "rect(20px, 10px, 50px, 8px); " + + "auto", + { + comp0: "rect(10px, 10px, 10px, 10px)", + comp1_6: "rect(10px, 10px, 10px, 10px)", + comp1_3: "rect(20px, 10px, 50px, 8px)", + comp2_3: "auto", + comp1: "auto", + } + ), + new AnimTestcasePaced( + "auto; " + "auto; " + "rect(20px, 30px, 130px, 4px)", + { + comp0: "auto", + comp1_6: "auto", + comp1_3: "auto", + comp2_3: "rect(20px, 30px, 130px, 4px)", + comp1: "rect(20px, 30px, 130px, 4px)", + } + ), + new AnimTestcasePaced("auto; auto; auto", { + comp0: "auto", + comp1_6: "auto", + comp1_3: "auto", + comp2_3: "auto", + comp1: "auto", + }), + ], +}; + +// TODO: test more properties here. +var gPacedBundles = [ + new TestcaseBundle(gPropList.clip, _pacedTestLists.rect), + new TestcaseBundle( + gPropList.color, + [].concat(_pacedTestLists.color, _pacedTestLists.currentColor_color) + ), + new TestcaseBundle(gPropList.direction, [ + new AnimTestcasePaced("rtl; ltr; rtl"), + ]), + new TestcaseBundle( + gPropList.fill, + [].concat( + _pacedTestLists.color, + _pacedTestLists.currentColor_fill, + _pacedTestLists.paintServer + ) + ), + new TestcaseBundle( + gPropList.font_size, + [].concat(_pacedTestLists.lengthNoUnits, _pacedTestLists.lengthPx, [ + new AnimTestcasePaced("20%; 24%; 16%", { + comp0: "10px", + comp1_6: "11px", + comp1_3: "12px", + comp2_3: "10px", + comp1: "8px", + }), + new AnimTestcasePaced("0px; 4%; 6px", { + comp0: "0px", + comp1_6: "1px", + comp1_3: "2px", + comp2_3: "4px", + comp1: "6px", + }), + ]) + ), + new TestcaseBundle(gPropList.font_size_adjust, [ + new AnimTestcasePaced("0.2; 0.6; 0.8", { + comp0: "0.2", + comp1_6: "0.3", + comp1_3: "0.4", + comp2_3: "0.6", + comp1: "0.8", + }), + new AnimTestcasePaced("none; none; 0.5", { + comp0: "none", + comp1_6: "none", + comp1_3: "none", + comp2_3: "0.5", + comp1: "0.5", + }), + ]), + new TestcaseBundle(gPropList.font_family, [ + // Sanity check: 'font-family' isn't interpolatable. It should end up + // ignoring the calcMode="paced" and falling into discrete-mode. + new AnimTestcasePaced( + "serif; sans-serif; monospace", + { + comp0: "serif", + comp1_6: "serif", + comp1_3: "sans-serif", + comp2_3: "monospace", + comp1: "monospace", + }, + "need support for more font properties" + ), + ]), + new TestcaseBundle(gPropList.opacity, _pacedTestLists.opacity), + new TestcaseBundle( + gPropList.stroke_dasharray, + [].concat(_pacedTestLists.lengthPctSVG, [ + new AnimTestcasePaced("7, 7, 7; 7, 10, 3; 1, 2, 3", { + comp0: "7px, 7px, 7px", + comp1_6: "7px, 8.5px, 5px", + comp1_3: "7px, 10px, 3px", + comp2_3: "4px, 6px, 3px", + comp1: "1px, 2px, 3px", + }), + ]) + ), + new TestcaseBundle( + gPropList.stroke_dashoffset, + [].concat( + _pacedTestLists.lengthNoUnits, + _pacedTestLists.lengthPx, + _pacedTestLists.lengthPctSVG, + _pacedTestLists.lengthPxPctSVG + ) + ), + new TestcaseBundle( + gPropList.stroke_width, + [].concat( + _pacedTestLists.lengthNoUnits, + _pacedTestLists.lengthPx, + _pacedTestLists.lengthPctSVG, + _pacedTestLists.lengthPxPctSVG + ) + ), +]; diff --git a/dom/smil/test/db_smilCSSPropertyList.js b/dom/smil/test/db_smilCSSPropertyList.js new file mode 100644 index 0000000000..237c9db585 --- /dev/null +++ b/dom/smil/test/db_smilCSSPropertyList.js @@ -0,0 +1,104 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* list of CSS properties recognized by SVG 1.1 spec, for use in mochitests */ + +// List of CSS Properties from SVG 1.1 Specification, Appendix N +var gPropList = { + // NOTE: AnimatedAttribute signature is: + // (attrName, attrType, sampleTarget, isAnimatable, isAdditive) + + // SKIP 'alignment-baseline' property: animatable but not supported by Mozilla + // SKIP 'baseline-shift' property: animatable but not supported by Mozilla + clip: new AdditiveAttribute("clip", "CSS", "marker"), + clip_path: new NonAdditiveAttribute("clip-path", "CSS", "rect"), + clip_rule: new NonAdditiveAttribute("clip-rule", "CSS", "circle"), + color: new AdditiveAttribute("color", "CSS", "rect"), + color_interpolation: new NonAdditiveAttribute( + "color-interpolation", + "CSS", + "rect" + ), + color_interpolation_filters: new NonAdditiveAttribute( + "color-interpolation-filters", + "CSS", + "feFlood" + ), + // SKIP 'color-profile' property: animatable but not supported by Mozilla + cursor: new NonAdditiveAttribute("cursor", "CSS", "rect"), + direction: new NonAnimatableAttribute("direction", "CSS", "text"), + display: new NonAdditiveAttribute("display", "CSS", "rect"), + dominant_baseline: new NonAdditiveAttribute( + "dominant-baseline", + "CSS", + "text" + ), + enable_background: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + new NonAnimatableAttribute("enable-background", "CSS", "marker"), + fill: new AdditiveAttribute("fill", "CSS", "rect"), + fill_opacity: new AdditiveAttribute("fill-opacity", "CSS", "rect"), + fill_rule: new NonAdditiveAttribute("fill-rule", "CSS", "rect"), + filter: new NonAdditiveAttribute("filter", "CSS", "rect"), + flood_color: new AdditiveAttribute("flood-color", "CSS", "feFlood"), + flood_opacity: new AdditiveAttribute("flood-opacity", "CSS", "feFlood"), + font: new NonAdditiveAttribute("font", "CSS", "text"), + font_family: new NonAdditiveAttribute("font-family", "CSS", "text"), + font_size: new AdditiveAttribute("font-size", "CSS", "text"), + font_size_adjust: new NonAdditiveAttribute("font-size-adjust", "CSS", "text"), + font_stretch: new NonAdditiveAttribute("font-stretch", "CSS", "text"), + font_style: new NonAdditiveAttribute("font-style", "CSS", "text"), + font_variant: new NonAdditiveAttribute("font-variant", "CSS", "text"), + // XXXdholbert should 'font-weight' be additive? + font_weight: new NonAdditiveAttribute("font-weight", "CSS", "text"), + glyph_orientation_horizontal: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "CSS", "text"), + glyph_orientation_vertical: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "CSS", "text"), + image_rendering: NonAdditiveAttribute("image-rendering", "CSS", "image"), + // SKIP 'kerning' property: animatable but not supported by Mozilla + letter_spacing: new AdditiveAttribute("letter-spacing", "CSS", "text"), + lighting_color: new AdditiveAttribute( + "lighting-color", + "CSS", + "feDiffuseLighting" + ), + marker: new NonAdditiveAttribute("marker", "CSS", "line"), + marker_end: new NonAdditiveAttribute("marker-end", "CSS", "line"), + marker_mid: new NonAdditiveAttribute("marker-mid", "CSS", "line"), + marker_start: new NonAdditiveAttribute("marker-start", "CSS", "line"), + mask: new NonAdditiveAttribute("mask", "CSS", "line"), + opacity: new AdditiveAttribute("opacity", "CSS", "rect"), + overflow: new NonAdditiveAttribute("overflow", "CSS", "marker"), + pointer_events: new NonAdditiveAttribute("pointer-events", "CSS", "rect"), + shape_rendering: new NonAdditiveAttribute("shape-rendering", "CSS", "rect"), + stop_color: new AdditiveAttribute("stop-color", "CSS", "stop"), + stop_opacity: new AdditiveAttribute("stop-opacity", "CSS", "stop"), + stroke: new AdditiveAttribute("stroke", "CSS", "rect"), + stroke_dasharray: new NonAdditiveAttribute("stroke-dasharray", "CSS", "rect"), + stroke_dashoffset: new AdditiveAttribute("stroke-dashoffset", "CSS", "rect"), + stroke_linecap: new NonAdditiveAttribute("stroke-linecap", "CSS", "rect"), + stroke_linejoin: new NonAdditiveAttribute("stroke-linejoin", "CSS", "rect"), + stroke_miterlimit: new AdditiveAttribute("stroke-miterlimit", "CSS", "rect"), + stroke_opacity: new AdditiveAttribute("stroke-opacity", "CSS", "rect"), + stroke_width: new AdditiveAttribute("stroke-width", "CSS", "rect"), + text_anchor: new NonAdditiveAttribute("text-anchor", "CSS", "text"), + text_decoration_line: new NonAdditiveAttribute( + "text-decoration-line", + "CSS", + "text" + ), + text_rendering: new NonAdditiveAttribute("text-rendering", "CSS", "text"), + unicode_bidi: new NonAnimatableAttribute("unicode-bidi", "CSS", "text"), + vector_effect: new NonAdditiveAttribute("vector-effect", "CSS", "rect"), + visibility: new NonAdditiveAttribute("visibility", "CSS", "rect"), + word_spacing: new AdditiveAttribute("word-spacing", "CSS", "text"), + writing_mode: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + new NonAnimatableAttribute("writing-mode", "CSS", "text"), +}; diff --git a/dom/smil/test/db_smilMappedAttrList.js b/dom/smil/test/db_smilMappedAttrList.js new file mode 100644 index 0000000000..81f71ef32b --- /dev/null +++ b/dom/smil/test/db_smilMappedAttrList.js @@ -0,0 +1,148 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* List of SVG presentational attributes in the SVG 1.1 spec, for use in + mochitests. (These are the attributes that are mapped to CSS properties) */ + +var gMappedAttrList = { + // NOTE: The list here should match the MappedAttributeEntry arrays in + // SVGElement.cpp + + // PresentationAttributes-FillStroke + fill: new AdditiveAttribute("fill", "XML", "rect"), + fill_opacity: new AdditiveAttribute("fill-opacity", "XML", "rect"), + fill_rule: new NonAdditiveAttribute("fill-rule", "XML", "rect"), + stroke: new AdditiveAttribute("stroke", "XML", "rect"), + stroke_dasharray: new NonAdditiveAttribute("stroke-dasharray", "XML", "rect"), + stroke_dashoffset: new AdditiveAttribute("stroke-dashoffset", "XML", "rect"), + stroke_linecap: new NonAdditiveAttribute("stroke-linecap", "XML", "rect"), + stroke_linejoin: new NonAdditiveAttribute("stroke-linejoin", "XML", "rect"), + stroke_miterlimit: new AdditiveAttribute("stroke-miterlimit", "XML", "rect"), + stroke_opacity: new AdditiveAttribute("stroke-opacity", "XML", "rect"), + stroke_width: new AdditiveAttribute("stroke-width", "XML", "rect"), + + // PresentationAttributes-Graphics + clip_path: new NonAdditiveAttribute("clip-path", "XML", "rect"), + clip_rule: new NonAdditiveAttribute("clip-rule", "XML", "circle"), + color_interpolation: new NonAdditiveAttribute( + "color-interpolation", + "XML", + "rect" + ), + cursor: new NonAdditiveAttribute("cursor", "XML", "rect"), + display: new NonAdditiveAttribute("display", "XML", "rect"), + filter: new NonAdditiveAttribute("filter", "XML", "rect"), + image_rendering: NonAdditiveAttribute("image-rendering", "XML", "image"), + mask: new NonAdditiveAttribute("mask", "XML", "line"), + pointer_events: new NonAdditiveAttribute("pointer-events", "XML", "rect"), + shape_rendering: new NonAdditiveAttribute("shape-rendering", "XML", "rect"), + text_rendering: new NonAdditiveAttribute("text-rendering", "XML", "text"), + visibility: new NonAdditiveAttribute("visibility", "XML", "rect"), + + // PresentationAttributes-TextContentElements + // SKIP 'alignment-baseline' property: animatable but not supported by Mozilla + // SKIP 'baseline-shift' property: animatable but not supported by Mozilla + direction: new NonAnimatableAttribute("direction", "XML", "text"), + dominant_baseline: new NonAdditiveAttribute( + "dominant-baseline", + "XML", + "text" + ), + glyph_orientation_horizontal: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "XML", "text"), + glyph_orientation_vertical: + // NOTE: Not supported by Mozilla, but explicitly non-animatable + NonAnimatableAttribute("glyph-orientation-horizontal", "XML", "text"), + // SKIP 'kerning' property: animatable but not supported by Mozilla + letter_spacing: new AdditiveAttribute("letter-spacing", "XML", "text"), + text_anchor: new NonAdditiveAttribute("text-anchor", "XML", "text"), + text_decoration_line: new NonAdditiveAttribute( + "text-decoration-line", + "XML", + "text" + ), + unicode_bidi: new NonAnimatableAttribute("unicode-bidi", "XML", "text"), + word_spacing: new AdditiveAttribute("word-spacing", "XML", "text"), + + // PresentationAttributes-FontSpecification + font_family: new NonAdditiveAttribute("font-family", "XML", "text"), + font_size: new AdditiveAttribute("font-size", "XML", "text"), + font_size_adjust: new NonAdditiveAttribute("font-size-adjust", "XML", "text"), + font_stretch: new NonAdditiveAttribute("font-stretch", "XML", "text"), + font_style: new NonAdditiveAttribute("font-style", "XML", "text"), + font_variant: new NonAdditiveAttribute("font-variant", "XML", "text"), + font_weight: new NonAdditiveAttribute("font-weight", "XML", "text"), + + // PresentationAttributes-GradientStop + stop_color: new AdditiveAttribute("stop-color", "XML", "stop"), + stop_opacity: new AdditiveAttribute("stop-opacity", "XML", "stop"), + + // PresentationAttributes-Viewports + overflow: new NonAdditiveAttribute("overflow", "XML", "marker"), + clip: new AdditiveAttribute("clip", "XML", "marker"), + + // PresentationAttributes-Makers + marker_end: new NonAdditiveAttribute("marker-end", "XML", "line"), + marker_mid: new NonAdditiveAttribute("marker-mid", "XML", "line"), + marker_start: new NonAdditiveAttribute("marker-start", "XML", "line"), + + // PresentationAttributes-Color + color: new AdditiveAttribute("color", "XML", "rect"), + + // PresentationAttributes-Filters + color_interpolation_filters: new NonAdditiveAttribute( + "color-interpolation-filters", + "XML", + "feFlood" + ), + + // PresentationAttributes-feFlood + flood_color: new AdditiveAttribute("flood-color", "XML", "feFlood"), + flood_opacity: new AdditiveAttribute("flood-opacity", "XML", "feFlood"), + + // PresentationAttributes-LightingEffects + lighting_color: new AdditiveAttribute( + "lighting-color", + "XML", + "feDiffuseLighting" + ), +}; + +// Utility method to copy a list of TestcaseBundle objects for CSS properties +// into a list of TestcaseBundles for the corresponding mapped attributes. +function convertCSSBundlesToMappedAttr(bundleList) { + // Create mapping of property names to the corresponding + // mapped-attribute object in gMappedAttrList. + var propertyNameToMappedAttr = {}; + for (attributeLabel in gMappedAttrList) { + var propName = gMappedAttrList[attributeLabel].attrName; + propertyNameToMappedAttr[propName] = gMappedAttrList[attributeLabel]; + } + + var convertedBundles = []; + for (var bundleIdx in bundleList) { + var origBundle = bundleList[bundleIdx]; + var propName = origBundle.animatedAttribute.attrName; + if (propertyNameToMappedAttr[propName]) { + // There's a mapped attribute by this name! Duplicate the TestcaseBundle, + // using the Mapped Attribute instead of the CSS Property. + is( + origBundle.animatedAttribute.attrType, + "CSS", + "expecting to be converting from CSS to XML" + ); + convertedBundles.push( + new TestcaseBundle( + propertyNameToMappedAttr[propName], + origBundle.testcaseList, + origBundle.skipReason + ) + ); + } + } + return convertedBundles; +} diff --git a/dom/smil/test/file_smilWithTransition.html b/dom/smil/test/file_smilWithTransition.html new file mode 100644 index 0000000000..b91398436b --- /dev/null +++ b/dom/smil/test/file_smilWithTransition.html @@ -0,0 +1,79 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=1315874 +--> +<head> + <meta charset="utf-8"> + <title>Test SMIL does not trigger CSS Transitions (bug 1315874)</title> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1315874">Mozilla Bug + 1315874</a> +<svg> + <rect width="100%" height="100%" + style="fill: red; transition: fill 10s" id="rect"> + <animate attributeName="fill" to="lime" dur="1s" fill="freeze"> + </rect> +</svg> +<script type="text/javascript"> + // Bring SimpleTest's function from opener. + if (opener) { + var is = opener.is.bind(opener); + var ok = opener.ok.bind(opener); + function finish() { + var o = opener; + self.close(); + o.SimpleTest.finish(); + } + } + + window.addEventListener('load', runTests); + + var rect = document.getElementById('rect'); + var svg = document.getElementsByTagName('svg')[0]; + is(getComputedStyle(rect).fill, 'rgb(255, 0, 0)', + 'The initial color should be red.'); + + function runTests() { + waitForFrame().then(function() { + svg.setCurrentTime(1); + is(getComputedStyle(rect).fill, 'rgb(0, 255, 0)', + 'The end color should be lime.'); + + return waitForAnimationFrames(2); + }).then(function() { + var anim = document.getAnimations()[0]; + ok(!anim, 'Transition should not be created by restyling for SMIL'); + finish(); + }); + } + + // Utility methods from testcommon.js + // For detail, see dom/animation/test/testcommon.js. + + function waitForFrame() { + return new Promise(function(resolve, reject) { + requestAnimationFrame(function(time) { + resolve(); + }); + }); + } + + function waitForAnimationFrames(frameCount) { + return new Promise(function(resolve, reject) { + function handleFrame() { + if (--frameCount <= 0) { + resolve(); + } else { + window.requestAnimationFrame(handleFrame); + } + } + window.requestAnimationFrame(handleFrame); + }); + } +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/mochitest.ini b/dom/smil/test/mochitest.ini new file mode 100644 index 0000000000..1b81646dbc --- /dev/null +++ b/dom/smil/test/mochitest.ini @@ -0,0 +1,63 @@ +[DEFAULT] +support-files = + db_smilAnimateMotion.js + db_smilCSSFromBy.js + db_smilCSSFromTo.js + db_smilCSSPaced.js + db_smilCSSPropertyList.js + db_smilMappedAttrList.js + file_smilWithTransition.html + smilAnimateMotionValueLists.js + smilExtDoc_helper.svg + smilTestUtils.js + smilXHR_helper.svg + +[test_smilAccessKey.xhtml] +[test_smilAdditionFallback.html] +[test_smilAnimateMotion.xhtml] +[test_smilAnimateMotionInvalidValues.xhtml] +[test_smilAnimateMotionOverrideRules.xhtml] +[test_smilBackwardsSeeking.xhtml] +[test_smilCSSFontStretchRelative.xhtml] +[test_smilCSSFromBy.xhtml] +[test_smilCSSFromTo.xhtml] +[test_smilCSSInherit.xhtml] +disabled=until bug 501183 is fixed +[test_smilCSSInvalidValues.xhtml] +[test_smilCSSPaced.xhtml] +[test_smilChangeAfterFrozen.xhtml] +skip-if = true # bug 1358955. +[test_smilConditionalProcessing.html] +[test_smilContainerBinding.xhtml] +[test_smilCrossContainer.xhtml] +[test_smilDynamicDelayedBeginElement.xhtml] +[test_smilExtDoc.xhtml] +[test_smilFillMode.xhtml] +[test_smilGetSimpleDuration.xhtml] +[test_smilGetStartTime.xhtml] +[test_smilHyperlinking.xhtml] +[test_smilInvalidValues.html] +[test_smilKeySplines.xhtml] +[test_smilKeyTimes.xhtml] +[test_smilKeyTimesPacedMode.xhtml] +[test_smilMappedAttrFromBy.xhtml] +[test_smilMappedAttrFromTo.xhtml] +[test_smilMappedAttrPaced.xhtml] +[test_smilMinTiming.html] +[test_smilRepeatDuration.html] +[test_smilRepeatTiming.xhtml] +[test_smilReset.xhtml] +[test_smilRestart.xhtml] +[test_smilSetCurrentTime.xhtml] +[test_smilSync.xhtml] +[test_smilSyncTransform.xhtml] +[test_smilSyncbaseTarget.xhtml] +[test_smilTextZoom.xhtml] +[test_smilTiming.xhtml] +[test_smilTimingZeroIntervals.xhtml] +[test_smilUpdatedInterval.xhtml] +[test_smilValues.xhtml] +[test_smilWithTransition.html] +skip-if = toolkit == 'android' && !is_fennec +[test_smilWithXlink.xhtml] +[test_smilXHR.xhtml] diff --git a/dom/smil/test/smilAnimateMotionValueLists.js b/dom/smil/test/smilAnimateMotionValueLists.js new file mode 100644 index 0000000000..6e05ebd7e1 --- /dev/null +++ b/dom/smil/test/smilAnimateMotionValueLists.js @@ -0,0 +1,116 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* Lists of valid & invalid values for the various <animateMotion> attributes */ +const gValidValues = [ + "10 10", + "10 10;", // Trailing semicolons are allowed + "10 10; ", + " 10 10em ", + "1 2 ; 3,4", + "1,2;3,4", + "0 0", + "0,0", +]; + +const gInvalidValues = [ + ";10 10", + "10 10;;", + "1 2 3", + "1 2 3 4", + "1,2;3,4 ,", + ",", + " , ", + ";", + " ; ", + "a", + " a; ", + ";a;", + "", + " ", + "1,2;3,4,", + "1,,2", + ",1,2", +]; + +const gValidRotate = [ + "10", + "20.1", + "30.5deg", + "0.5rad", + "auto", + "auto-reverse", + " 10 ", + " 10deg", + "10deg ", + " 10.1 ", +]; + +const gInvalidRotate = ["10 deg", "10 rad ", "aaa"]; + +const gValidToBy = ["0 0", "1em,2", "50.3em 0.2in", " 1,2", "1 2 "]; + +const gInvalidToBy = [ + "0 0 0", + "0 0,0", + "0,0,0", + "1emm 2", + "1 2;", + "1 2,", + " 1,2 ,", + "abc", + ",", + "", + "1,,2", + "1,2,", +]; + +const gValidPath = [ + "m0 0 L30 30", + "M20,20L10 10", + "M20,20 L30, 30h20", + "m50 50", + "M50 50", + "m0 0", + "M0, 0", +]; + +// paths must start with at least a valid "M" segment to be valid +const gInvalidPath = ["M20in 20", "h30", "L50 50", "abc"]; + +// paths that at least start with a valid "M" segment are valid - the spec says +// to parse everything up to the first invalid token +const gValidPathWithErrors = ["M20 20em", "m0 0 L30,,30", "M10 10 L50 50 abc"]; + +const gValidKeyPoints = [ + "0; 0.5; 1", + "0;.5;1", + "0; 0; 1", + "0; 1; 1", + "0; 0; 1;", // Trailing semicolons are allowed + "0; 0; 1; ", + "0; 0.000; 1", + "0; 0.000001; 1", +]; + +// Should have 3 values to be valid. +// Same as number of keyTimes values +const gInvalidKeyPoints = [ + "0; 1", + "0; 0.5; 0.75; 1", + "0; 1;", + "0", + "1", + "a", + "", + " ", + "0; -0.1; 1", + "0; 1.1; 1", + "0; 0.1; 1.1", + "-0.1; 0.1; 1", + "0; a; 1", + "0;;1", +]; diff --git a/dom/smil/test/smilExtDoc_helper.svg b/dom/smil/test/smilExtDoc_helper.svg new file mode 100644 index 0000000000..fbd9d091a4 --- /dev/null +++ b/dom/smil/test/smilExtDoc_helper.svg @@ -0,0 +1,7 @@ +<svg xmlns="http://www.w3.org/2000/svg"> + <filter id="filter"> + <feFlood flood-color="red"> + <set attributeName="flood-color" to="lime" begin="0.001"/> + </feFlood> + </filter> +</svg> diff --git a/dom/smil/test/smilTestUtils.js b/dom/smil/test/smilTestUtils.js new file mode 100644 index 0000000000..8b6bddd4e1 --- /dev/null +++ b/dom/smil/test/smilTestUtils.js @@ -0,0 +1,1014 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ts=2 sw=2 sts=2 et: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// Note: Class syntax roughly based on: +// https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Inheritance +const SVG_NS = "http://www.w3.org/2000/svg"; +const XLINK_NS = "http://www.w3.org/1999/xlink"; + +const MPATH_TARGET_ID = "smilTestUtilsTestingPath"; + +function extend(child, supertype) { + child.prototype.__proto__ = supertype.prototype; +} + +// General Utility Methods +var SMILUtil = { + // Returns the first matched <svg> node in the document + getSVGRoot() { + return SMILUtil.getFirstElemWithTag("svg"); + }, + + // Returns the first element in the document with the matching tag + getFirstElemWithTag(aTargetTag) { + var elemList = document.getElementsByTagName(aTargetTag); + return !elemList.length ? null : elemList[0]; + }, + + // Simple wrapper for getComputedStyle + getComputedStyleSimple(elem, prop) { + return window.getComputedStyle(elem).getPropertyValue(prop); + }, + + getAttributeValue(elem, attr) { + if (attr.attrName == SMILUtil.getMotionFakeAttributeName()) { + // Fake motion "attribute" -- "computed value" is the element's CTM + return elem.getCTM(); + } + if (attr.attrType == "CSS") { + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); + } + if (attr.attrType == "XML") { + // XXXdholbert This is appropriate for mapped attributes, but not + // for other attributes. + return SMILUtil.getComputedStyleWrapper(elem, attr.attrName); + } + }, + + // Smart wrapper for getComputedStyle, which will generate a "fake" computed + // style for recognized shorthand properties (font, font-variant, overflow, marker) + getComputedStyleWrapper(elem, propName) { + // Special cases for shorthand properties (which aren't directly queriable + // via getComputedStyle) + var computedStyle; + if (propName == "font") { + var subProps = [ + "font-style", + "font-variant-caps", + "font-weight", + "font-size", + "line-height", + "font-family", + ]; + for (var i in subProps) { + var subPropStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + if (subPropStyle) { + if (subProps[i] == "line-height") { + // There needs to be a "/" before line-height + subPropStyle = "/ " + subPropStyle; + } + if (!computedStyle) { + computedStyle = subPropStyle; + } else { + computedStyle = computedStyle + " " + subPropStyle; + } + } + } + } else if (propName == "font-variant") { + // xxx - this isn't completely correct but it's sufficient for what's + // being tested here + computedStyle = SMILUtil.getComputedStyleSimple( + elem, + "font-variant-caps" + ); + } else if (propName == "marker") { + var subProps = ["marker-end", "marker-mid", "marker-start"]; + for (var i in subProps) { + if (!computedStyle) { + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + } else { + is( + computedStyle, + SMILUtil.getComputedStyleSimple(elem, subProps[i]), + "marker sub-properties should match each other " + + "(they shouldn't be individually set)" + ); + } + } + } else if (propName == "overflow") { + var subProps = ["overflow-x", "overflow-y"]; + for (var i in subProps) { + if (!computedStyle) { + computedStyle = SMILUtil.getComputedStyleSimple(elem, subProps[i]); + } else { + is( + computedStyle, + SMILUtil.getComputedStyleSimple(elem, subProps[i]), + "overflow sub-properties should match each other " + + "(they shouldn't be individually set)" + ); + } + } + } else { + computedStyle = SMILUtil.getComputedStyleSimple(elem, propName); + } + return computedStyle; + }, + + getMotionFakeAttributeName() { + return "_motion"; + }, + + // Return stripped px value from specified value. + stripPx: str => str.replace(/px\s*$/, ""), +}; + +var CTMUtil = { + CTM_COMPONENTS_ALL: ["a", "b", "c", "d", "e", "f"], + CTM_COMPONENTS_ROTATE: ["a", "b", "c", "d"], + + // Function to generate a CTM Matrix from a "summary" + // (a 3-tuple containing [tX, tY, theta]) + generateCTM(aCtmSummary) { + if (!aCtmSummary || aCtmSummary.length != 3) { + ok(false, "Unexpected CTM summary tuple length: " + aCtmSummary.length); + } + var tX = aCtmSummary[0]; + var tY = aCtmSummary[1]; + var theta = aCtmSummary[2]; + var cosTheta = Math.cos(theta); + var sinTheta = Math.sin(theta); + var newCtm = { + a: cosTheta, + c: -sinTheta, + e: tX, + b: sinTheta, + d: cosTheta, + f: tY, + }; + return newCtm; + }, + + /// Helper for isCtmEqual + isWithinDelta(aTestVal, aExpectedVal, aErrMsg, aIsTodo) { + var testFunc = aIsTodo ? todo : ok; + const delta = 0.00001; // allowing margin of error = 10^-5 + ok( + aTestVal >= aExpectedVal - delta && aTestVal <= aExpectedVal + delta, + aErrMsg + " | got: " + aTestVal + ", expected: " + aExpectedVal + ); + }, + + assertCTMEqual(aLeftCtm, aRightCtm, aComponentsToCheck, aErrMsg, aIsTodo) { + var foundCTMDifference = false; + for (var j in aComponentsToCheck) { + var curComponent = aComponentsToCheck[j]; + if (!aIsTodo) { + CTMUtil.isWithinDelta( + aLeftCtm[curComponent], + aRightCtm[curComponent], + aErrMsg + " | component: " + curComponent, + false + ); + } else if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { + foundCTMDifference = true; + } + } + + if (aIsTodo) { + todo(!foundCTMDifference, aErrMsg + " | (currently marked todo)"); + } + }, + + assertCTMNotEqual(aLeftCtm, aRightCtm, aComponentsToCheck, aErrMsg, aIsTodo) { + // CTM should not match initial one + var foundCTMDifference = false; + for (var j in aComponentsToCheck) { + var curComponent = aComponentsToCheck[j]; + if (aLeftCtm[curComponent] != aRightCtm[curComponent]) { + foundCTMDifference = true; + break; // We found a difference, as expected. Success! + } + } + + if (aIsTodo) { + todo(foundCTMDifference, aErrMsg + " | (currently marked todo)"); + } else { + ok(foundCTMDifference, aErrMsg); + } + }, +}; + +// Wrapper for timing information +function SMILTimingData(aBegin, aDur) { + this._begin = aBegin; + this._dur = aDur; +} +SMILTimingData.prototype = { + _begin: null, + _dur: null, + getBeginTime() { + return this._begin; + }, + getDur() { + return this._dur; + }, + getEndTime() { + return this._begin + this._dur; + }, + getFractionalTime(aPortion) { + return this._begin + aPortion * this._dur; + }, +}; + +/** + * Attribute: a container for information about an attribute we'll + * attempt to animate with SMIL in our tests. + * + * See also the factory methods below: NonAnimatableAttribute(), + * NonAdditiveAttribute(), and AdditiveAttribute(). + * + * @param aAttrName The name of the attribute + * @param aAttrType The type of the attribute ("CSS" vs "XML") + * @param aTargetTag The name of an element that this attribute could be + * applied to. + * @param aIsAnimatable A bool indicating whether this attribute is defined as + * animatable in the SVG spec. + * @param aIsAdditive A bool indicating whether this attribute is defined as + * additive (i.e. supports "by" animation) in the SVG spec. + */ +function Attribute( + aAttrName, + aAttrType, + aTargetTag, + aIsAnimatable, + aIsAdditive +) { + this.attrName = aAttrName; + this.attrType = aAttrType; + this.targetTag = aTargetTag; + this.isAnimatable = aIsAnimatable; + this.isAdditive = aIsAdditive; +} +Attribute.prototype = { + // Member variables + attrName: null, + attrType: null, + isAnimatable: null, + testcaseList: null, +}; + +// Generators for Attribute objects. These allow lists of attribute +// definitions to be more human-readible than if we were using Attribute() with +// boolean flags, e.g. "Attribute(..., true, true), Attribute(..., true, false) +function NonAnimatableAttribute(aAttrName, aAttrType, aTargetTag) { + return new Attribute(aAttrName, aAttrType, aTargetTag, false, false); +} +function NonAdditiveAttribute(aAttrName, aAttrType, aTargetTag) { + return new Attribute(aAttrName, aAttrType, aTargetTag, true, false); +} +function AdditiveAttribute(aAttrName, aAttrType, aTargetTag) { + return new Attribute(aAttrName, aAttrType, aTargetTag, true, true); +} + +/** + * TestcaseBundle: a container for a group of tests for a particular attribute + * + * @param aAttribute An Attribute object for the attribute + * @param aTestcaseList An array of AnimTestcase objects + */ +function TestcaseBundle(aAttribute, aTestcaseList, aSkipReason) { + this.animatedAttribute = aAttribute; + this.testcaseList = aTestcaseList; + this.skipReason = aSkipReason; +} +TestcaseBundle.prototype = { + // Member variables + animatedAttribute: null, + testcaseList: null, + skipReason: null, + + // Methods + go(aTimingData) { + if (this.skipReason) { + todo( + false, + "Skipping a bundle for '" + + this.animatedAttribute.attrName + + "' because: " + + this.skipReason + ); + } else { + // Sanity Check: Bundle should have > 0 testcases + if (!this.testcaseList || !this.testcaseList.length) { + ok( + false, + "a bundle for '" + + this.animatedAttribute.attrName + + "' has no testcases" + ); + } + + var targetElem = SMILUtil.getFirstElemWithTag( + this.animatedAttribute.targetTag + ); + + if (!targetElem) { + ok( + false, + "Error: can't find an element of type '" + + this.animatedAttribute.targetTag + + "', so I can't test property '" + + this.animatedAttribute.attrName + + "'" + ); + return; + } + + for (var testcaseIdx in this.testcaseList) { + var testcase = this.testcaseList[testcaseIdx]; + if (testcase.skipReason) { + todo( + false, + "Skipping a testcase for '" + + this.animatedAttribute.attrName + + "' because: " + + testcase.skipReason + ); + } else { + testcase.runTest( + targetElem, + this.animatedAttribute, + aTimingData, + false + ); + testcase.runTest( + targetElem, + this.animatedAttribute, + aTimingData, + true + ); + } + } + } + }, +}; + +/** + * AnimTestcase: an abstract class that represents an animation testcase. + * (e.g. a set of "from"/"to" values to test) + */ +function AnimTestcase() {} // abstract => no constructor +AnimTestcase.prototype = { + // Member variables + _animElementTagName: "animate", // Can be overridden for e.g. animateColor + computedValMap: null, + skipReason: null, + + // Methods + /** + * runTest: Runs this AnimTestcase + * + * @param aTargetElem The node to be targeted in our test animation. + * @param aTargetAttr An Attribute object representing the attribute + * to be targeted in our test animation. + * @param aTimeData A SMILTimingData object with timing information for + * our test animation. + * @param aIsFreeze If true, indicates that our test animation should use + * fill="freeze"; otherwise, we'll default to fill="remove". + */ + runTest(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) { + // SANITY CHECKS + if (!SMILUtil.getSVGRoot().animationsPaused()) { + ok(false, "Should start each test with animations paused"); + } + if (SMILUtil.getSVGRoot().getCurrentTime() != 0) { + ok(false, "Should start each test at time = 0"); + } + + // SET UP + // Cache initial computed value + var baseVal = SMILUtil.getAttributeValue(aTargetElem, aTargetAttr); + + // Create & append animation element + var anim = this.setupAnimationElement(aTargetAttr, aTimeData, aIsFreeze); + aTargetElem.appendChild(anim); + + // Build a list of [seek-time, expectedValue, errorMessage] triplets + var seekList = this.buildSeekList( + aTargetAttr, + baseVal, + aTimeData, + aIsFreeze + ); + + // DO THE ACTUAL TESTING + this.seekAndTest(seekList, aTargetElem, aTargetAttr); + + // CLEAN UP + aTargetElem.removeChild(anim); + SMILUtil.getSVGRoot().setCurrentTime(0); + }, + + // HELPER FUNCTIONS + // setupAnimationElement: <animate> element + // Subclasses should extend this parent method + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + var animElement = document.createElementNS( + SVG_NS, + this._animElementTagName + ); + animElement.setAttribute("attributeName", aAnimAttr.attrName); + animElement.setAttribute("attributeType", aAnimAttr.attrType); + animElement.setAttribute("begin", aTimeData.getBeginTime()); + animElement.setAttribute("dur", aTimeData.getDur()); + if (aIsFreeze) { + animElement.setAttribute("fill", "freeze"); + } + return animElement; + }, + + buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + if (!aAnimAttr.isAnimatable) { + return this.buildSeekListStatic( + aAnimAttr, + aBaseVal, + aTimeData, + "defined as non-animatable in SVG spec" + ); + } + if (this.computedValMap.noEffect) { + return this.buildSeekListStatic( + aAnimAttr, + aBaseVal, + aTimeData, + "testcase specified to have no effect" + ); + } + return this.buildSeekListAnimated( + aAnimAttr, + aBaseVal, + aTimeData, + aIsFreeze + ); + }, + + seekAndTest(aSeekList, aTargetElem, aTargetAttr) { + var svg = document.getElementById("svg"); + for (var i in aSeekList) { + var entry = aSeekList[i]; + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); + + // Bug 1379908: The computed value of stroke-* properties should be + // serialized with px units, but currently Gecko and Servo don't do that + // when animating these values. + if ( + ["stroke-width", "stroke-dasharray", "stroke-dashoffset"].includes( + aTargetAttr.attrName + ) + ) { + var attr = SMILUtil.stripPx( + SMILUtil.getAttributeValue(aTargetElem, aTargetAttr) + ); + var expectedVal = SMILUtil.stripPx(entry[1]); + is(attr, expectedVal, entry[2]); + return; + } + is( + SMILUtil.getAttributeValue(aTargetElem, aTargetAttr), + entry[1], + entry[2] + ); + } + }, + + // methods that expect to be overridden in subclasses + buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) {}, + buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) {}, +}; + +// Abstract parent class to share code between from-to & from-by testcases. +function AnimTestcaseFrom() {} // abstract => no constructor +AnimTestcaseFrom.prototype = { + // Member variables + from: null, + + // Methods + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + // Call super, and then add my own customization + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, [ + aAnimAttr, + aTimeData, + aIsFreeze, + ]); + animElem.setAttribute("from", this.from); + return animElem; + }, + + buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) { + var seekList = new Array(); + var msgPrefix = + aAnimAttr.attrName + ": shouldn't be affected by animation "; + seekList.push([ + aTimeData.getBeginTime(), + aBaseVal, + msgPrefix + "(at animation begin) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 2), + aBaseVal, + msgPrefix + "(at animation mid) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getEndTime(), + aBaseVal, + msgPrefix + "(at animation end) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + aBaseVal, + msgPrefix + "(after animation end) - " + aReasonStatic, + ]); + return seekList; + }, + + buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + ": "; + if (aTimeData.getBeginTime() > 0.1) { + seekList.push([ + aTimeData.getBeginTime() - 0.1, + aBaseVal, + msgPrefix + + "checking that base value is set " + + "before start of animation", + ]); + } + + seekList.push([ + aTimeData.getBeginTime(), + this.computedValMap.fromComp || this.from, + msgPrefix + + "checking that 'from' value is set " + + "at start of animation", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 2), + this.computedValMap.midComp || this.computedValMap.toComp || this.to, + msgPrefix + "checking value halfway through animation", + ]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = this.computedValMap.toComp || this.to; + finalMsg = msgPrefix + "[freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = + msgPrefix + "[remove-mode] checking that animation is cleared "; + } + seekList.push([ + aTimeData.getEndTime(), + expectedEndVal, + finalMsg + "at end of animation", + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, + finalMsg + "after end of animation", + ]); + return seekList; + }, +}; +extend(AnimTestcaseFrom, AnimTestcase); + +/* + * A testcase for a simple "from-to" animation + * @param aFrom The 'from' value + * @param aTo The 'to' value + * @param aComputedValMap A hash-map that contains some computed values, + * if they're needed, as follows: + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) + * - midComp: Computed value that we expect to visit halfway through the + * animation (if different from |aTo|) + * - toComp: Computed value version of |aTo| (if different from |aTo|) + * - noEffect: Special flag -- if set, indicates that this testcase is + * expected to have no effect on the computed value. (e.g. the + * given values are invalid.) + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + * + */ +function AnimTestcaseFromTo(aFrom, aTo, aComputedValMap, aSkipReason) { + this.from = aFrom; + this.to = aTo; + this.computedValMap = aComputedValMap || {}; // Let aComputedValMap be omitted + this.skipReason = aSkipReason; +} +AnimTestcaseFromTo.prototype = { + // Member variables + to: null, + + // Methods + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + // Call super, and then add my own customization + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply( + this, + [aAnimAttr, aTimeData, aIsFreeze] + ); + animElem.setAttribute("to", this.to); + return animElem; + }, +}; +extend(AnimTestcaseFromTo, AnimTestcaseFrom); + +/* + * A testcase for a simple "from-by" animation. + * + * @param aFrom The 'from' value + * @param aBy The 'by' value + * @param aComputedValMap A hash-map that contains some computed values that + * we expect to visit, as follows: + * - fromComp: Computed value version of |aFrom| (if different from |aFrom|) + * - midComp: Computed value that we expect to visit halfway through the + * animation (|aFrom| + |aBy|/2) + * - toComp: Computed value of the animation endpoint (|aFrom| + |aBy|) + * - noEffect: Special flag -- if set, indicates that this testcase is + * expected to have no effect on the computed value. (e.g. the + * given values are invalid. Or the attribute may be animatable + * and additive, but the particular "from" & "by" values that + * are used don't support addition.) + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimTestcaseFromBy(aFrom, aBy, aComputedValMap, aSkipReason) { + this.from = aFrom; + this.by = aBy; + this.computedValMap = aComputedValMap; + this.skipReason = aSkipReason; + if ( + this.computedValMap && + !this.computedValMap.noEffect && + !this.computedValMap.toComp + ) { + ok(false, "AnimTestcaseFromBy needs expected computed final value"); + } +} +AnimTestcaseFromBy.prototype = { + // Member variables + by: null, + + // Methods + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + // Call super, and then add my own customization + var animElem = AnimTestcaseFrom.prototype.setupAnimationElement.apply( + this, + [aAnimAttr, aTimeData, aIsFreeze] + ); + animElem.setAttribute("by", this.by); + return animElem; + }, + buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + if (!aAnimAttr.isAdditive) { + return this.buildSeekListStatic( + aAnimAttr, + aBaseVal, + aTimeData, + "defined as non-additive in SVG spec" + ); + } + // Just use inherited method + return AnimTestcaseFrom.prototype.buildSeekList.apply(this, [ + aAnimAttr, + aBaseVal, + aTimeData, + aIsFreeze, + ]); + }, +}; +extend(AnimTestcaseFromBy, AnimTestcaseFrom); + +/* + * A testcase for a "paced-mode" animation + * @param aValues An array of values, to be used as the "Values" list + * @param aComputedValMap A hash-map that contains some computed values, + * if they're needed, as follows: + * - comp0: The computed value at the start of the animation + * - comp1_6: The computed value exactly 1/6 through animation + * - comp1_3: The computed value exactly 1/3 through animation + * - comp2_3: The computed value exactly 2/3 through animation + * - comp1: The computed value of the animation endpoint + * The math works out easiest if... + * (a) aValuesString has 3 entries in its values list: vA, vB, vC + * (b) dist(vB, vC) = 2 * dist(vA, vB) + * With this setup, we can come up with expected intermediate values according + * to the following rules: + * - comp0 should be vA + * - comp1_6 should be us halfway between vA and vB + * - comp1_3 should be vB + * - comp2_3 should be halfway between vB and vC + * - comp1 should be vC + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimTestcasePaced(aValuesString, aComputedValMap, aSkipReason) { + this.valuesString = aValuesString; + this.computedValMap = aComputedValMap; + this.skipReason = aSkipReason; + if ( + this.computedValMap && + (!this.computedValMap.comp0 || + !this.computedValMap.comp1_6 || + !this.computedValMap.comp1_3 || + !this.computedValMap.comp2_3 || + !this.computedValMap.comp1) + ) { + ok(false, "This AnimTestcasePaced has an incomplete computed value map"); + } +} +AnimTestcasePaced.prototype = { + // Member variables + valuesString: null, + + // Methods + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + // Call super, and then add my own customization + var animElem = AnimTestcase.prototype.setupAnimationElement.apply(this, [ + aAnimAttr, + aTimeData, + aIsFreeze, + ]); + animElem.setAttribute("values", this.valuesString); + animElem.setAttribute("calcMode", "paced"); + return animElem; + }, + buildSeekListAnimated(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + var seekList = new Array(); + var msgPrefix = aAnimAttr.attrName + ": checking value "; + seekList.push([ + aTimeData.getBeginTime(), + this.computedValMap.comp0, + msgPrefix + "at start of animation", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 6), + this.computedValMap.comp1_6, + msgPrefix + "1/6 of the way through animation.", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 3), + this.computedValMap.comp1_3, + msgPrefix + "1/3 of the way through animation.", + ]); + seekList.push([ + aTimeData.getFractionalTime(2 / 3), + this.computedValMap.comp2_3, + msgPrefix + "2/3 of the way through animation.", + ]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = this.computedValMap.comp1; + finalMsg = + aAnimAttr.attrName + + ": [freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = + aAnimAttr.attrName + + ": [remove-mode] checking that animation is cleared "; + } + seekList.push([ + aTimeData.getEndTime(), + expectedEndVal, + finalMsg + "at end of animation", + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, + finalMsg + "after end of animation", + ]); + return seekList; + }, + buildSeekListStatic(aAnimAttr, aBaseVal, aTimeData, aReasonStatic) { + var seekList = new Array(); + var msgPrefix = + aAnimAttr.attrName + ": shouldn't be affected by animation "; + seekList.push([ + aTimeData.getBeginTime(), + aBaseVal, + msgPrefix + "(at animation begin) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 6), + aBaseVal, + msgPrefix + "(1/6 of the way through animation) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 3), + aBaseVal, + msgPrefix + "(1/3 of the way through animation) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getFractionalTime(2 / 3), + aBaseVal, + msgPrefix + "(2/3 of the way through animation) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getEndTime(), + aBaseVal, + msgPrefix + "(at animation end) - " + aReasonStatic, + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + aBaseVal, + msgPrefix + "(after animation end) - " + aReasonStatic, + ]); + return seekList; + }, +}; +extend(AnimTestcasePaced, AnimTestcase); + +/* + * A testcase for an <animateMotion> animation. + * + * @param aAttrValueHash A hash-map mapping attribute names to values. + * Should include at least 'path', 'values', 'to' + * or 'by' to describe the motion path. + * @param aCtmMap A hash-map that contains summaries of the expected resulting + * CTM at various points during the animation. The CTM is + * summarized as a tuple of three numbers: [tX, tY, theta] + (indicating a translate(tX,tY) followed by a rotate(theta)) + * - ctm0: The CTM summary at the start of the animation + * - ctm1_6: The CTM summary at exactly 1/6 through animation + * - ctm1_3: The CTM summary at exactly 1/3 through animation + * - ctm2_3: The CTM summary at exactly 2/3 through animation + * - ctm1: The CTM summary at the animation endpoint + * + * NOTE: For paced-mode animation (the default for animateMotion), the math + * works out easiest if: + * (a) our motion path has 3 points: vA, vB, vC + * (b) dist(vB, vC) = 2 * dist(vA, vB) + * (See discussion in header comment for AnimTestcasePaced.) + * + * @param aSkipReason If this test-case is known to currently fail, this + * parameter should be a string explaining why. + * Otherwise, this value should be null (or omitted). + */ +function AnimMotionTestcase(aAttrValueHash, aCtmMap, aSkipReason) { + this.attrValueHash = aAttrValueHash; + this.ctmMap = aCtmMap; + this.skipReason = aSkipReason; + if ( + this.ctmMap && + (!this.ctmMap.ctm0 || + !this.ctmMap.ctm1_6 || + !this.ctmMap.ctm1_3 || + !this.ctmMap.ctm2_3 || + !this.ctmMap.ctm1) + ) { + ok(false, "This AnimMotionTestcase has an incomplete CTM map"); + } +} +AnimMotionTestcase.prototype = { + // Member variables + _animElementTagName: "animateMotion", + + // Implementations of inherited methods that we need to override: + // -------------------------------------------------------------- + setupAnimationElement(aAnimAttr, aTimeData, aIsFreeze) { + var animElement = document.createElementNS( + SVG_NS, + this._animElementTagName + ); + animElement.setAttribute("begin", aTimeData.getBeginTime()); + animElement.setAttribute("dur", aTimeData.getDur()); + if (aIsFreeze) { + animElement.setAttribute("fill", "freeze"); + } + for (var attrName in this.attrValueHash) { + if (attrName == "mpath") { + this.createPath(this.attrValueHash[attrName]); + this.createMpath(animElement); + } else { + animElement.setAttribute(attrName, this.attrValueHash[attrName]); + } + } + return animElement; + }, + + createPath(aPathDescription) { + var path = document.createElementNS(SVG_NS, "path"); + path.setAttribute("d", aPathDescription); + path.setAttribute("id", MPATH_TARGET_ID); + return SMILUtil.getSVGRoot().appendChild(path); + }, + + createMpath(aAnimElement) { + var mpath = document.createElementNS(SVG_NS, "mpath"); + mpath.setAttributeNS(XLINK_NS, "href", "#" + MPATH_TARGET_ID); + return aAnimElement.appendChild(mpath); + }, + + // Override inherited seekAndTest method since... + // (a) it expects a computedValMap and we have a computed-CTM map instead + // and (b) it expects we might have no effect (for non-animatable attrs) + buildSeekList(aAnimAttr, aBaseVal, aTimeData, aIsFreeze) { + var seekList = new Array(); + var msgPrefix = "CTM mismatch "; + seekList.push([ + aTimeData.getBeginTime(), + CTMUtil.generateCTM(this.ctmMap.ctm0), + msgPrefix + "at start of animation", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 6), + CTMUtil.generateCTM(this.ctmMap.ctm1_6), + msgPrefix + "1/6 of the way through animation.", + ]); + seekList.push([ + aTimeData.getFractionalTime(1 / 3), + CTMUtil.generateCTM(this.ctmMap.ctm1_3), + msgPrefix + "1/3 of the way through animation.", + ]); + seekList.push([ + aTimeData.getFractionalTime(2 / 3), + CTMUtil.generateCTM(this.ctmMap.ctm2_3), + msgPrefix + "2/3 of the way through animation.", + ]); + + var finalMsg; + var expectedEndVal; + if (aIsFreeze) { + expectedEndVal = CTMUtil.generateCTM(this.ctmMap.ctm1); + finalMsg = + aAnimAttr.attrName + + ": [freeze-mode] checking that final value is set "; + } else { + expectedEndVal = aBaseVal; + finalMsg = + aAnimAttr.attrName + + ": [remove-mode] checking that animation is cleared "; + } + seekList.push([ + aTimeData.getEndTime(), + expectedEndVal, + finalMsg + "at end of animation", + ]); + seekList.push([ + aTimeData.getEndTime() + aTimeData.getDur(), + expectedEndVal, + finalMsg + "after end of animation", + ]); + return seekList; + }, + + // Override inherited seekAndTest method + // (Have to use assertCTMEqual() instead of is() for comparison, to check each + // component of the CTM and to allow for a small margin of error.) + seekAndTest(aSeekList, aTargetElem, aTargetAttr) { + var svg = document.getElementById("svg"); + for (var i in aSeekList) { + var entry = aSeekList[i]; + SMILUtil.getSVGRoot().setCurrentTime(entry[0]); + CTMUtil.assertCTMEqual( + aTargetElem.getCTM(), + entry[1], + CTMUtil.CTM_COMPONENTS_ALL, + entry[2], + false + ); + } + }, + + // Override "runTest" method so we can remove any <path> element that we + // created at the end of each test. + runTest(aTargetElem, aTargetAttr, aTimeData, aIsFreeze) { + AnimTestcase.prototype.runTest.apply(this, [ + aTargetElem, + aTargetAttr, + aTimeData, + aIsFreeze, + ]); + var pathElem = document.getElementById(MPATH_TARGET_ID); + if (pathElem) { + SMILUtil.getSVGRoot().removeChild(pathElem); + } + }, +}; +extend(AnimMotionTestcase, AnimTestcase); + +// MAIN METHOD +function testBundleList(aBundleList, aTimingData) { + for (var bundleIdx in aBundleList) { + aBundleList[bundleIdx].go(aTimingData); + } +} diff --git a/dom/smil/test/smilXHR_helper.svg b/dom/smil/test/smilXHR_helper.svg new file mode 100644 index 0000000000..cb0b51c360 --- /dev/null +++ b/dom/smil/test/smilXHR_helper.svg @@ -0,0 +1,8 @@ +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle id="circ" cx="20" cy="20" r="15" fill="blue"> + <animate id="animXML" attributeName="cx" attributeType="XML" + from="500" to="600" begin="0s" dur="4s"/> + <animate id="animCSS" attributeName="opacity" attributeType="CSS" + from="0.2" to="0.3" begin="0s" dur="4s"/> + </circle> +</svg> diff --git a/dom/smil/test/test_smilAccessKey.xhtml b/dom/smil/test/test_smilAccessKey.xhtml new file mode 100644 index 0000000000..5910dc1c27 --- /dev/null +++ b/dom/smil/test/test_smilAccessKey.xhtml @@ -0,0 +1,79 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL accessKey support</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=587910">Mozilla Bug + 587910</a> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="20" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for lack of SMIL accessKey support **/ + +const gSvgns = 'http://www.w3.org/2000/svg'; +SimpleTest.waitForExplicitFinish(); + +function main() +{ + testBeginValueIsNotSupported('accessKey(a)'); + testBeginValueIsNotSupported('accesskey(a)'); + + is(getStartTime('accesskey(a); 1s'), 1, + 'Start time for accesskey(a) followed by a literal time'); + is(getStartTime('3s; accessKey(a)'), 3, + 'Start time for accesskey(a) preceded by a literal time'); + + SimpleTest.finish(); +} + +function createAnim(beginSpec) { + const anim = document.createElementNS(gSvgns, 'animate'); + anim.setAttribute('attributeName', 'cx'); + anim.setAttribute('values', '0; 100'); + anim.setAttribute('dur', '10s'); + anim.setAttribute('begin', beginSpec); + return document.getElementById('circle').appendChild(anim); +} + +function testBeginValueIsNotSupported(beginSpec) { + const anim = createAnim(beginSpec); + + try { + anim.getStartTime(); + ok(false, + `Should have failed to get start time for begin value: ${beginSpec}`); + } catch(e) { + is(e.name, 'InvalidStateError', `Unexpected exception: ${e.name}`); + is(e.code, DOMException.INVALID_STATE_ERR, + `Unexpected exception code: ${e.code}`); + } + + anim.remove(); +} + +function getStartTime(beginSpec) { + const anim = createAnim(beginSpec); + let startTime; + try { + startTime = anim.getStartTime(); + } catch (e) { } + anim.remove(); + + return startTime; +} + +window.addEventListener('load', main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilAdditionFallback.html b/dom/smil/test/test_smilAdditionFallback.html new file mode 100644 index 0000000000..a73457728c --- /dev/null +++ b/dom/smil/test/test_smilAdditionFallback.html @@ -0,0 +1,30 @@ +<!doctype html> +<meta charset=utf-8> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<p id=display></p> +<div id=content> +<svg id=svg> +<!-- These two animations will have a default duration of indefinite which means + they will keep producing their first value forever --> +<animate calcMode="discrete" attributeName="height" by="10" dur="1s" + fill="freeze"/> +<animate calcMode="discrete" attributeName="height" by="10" dur="1s" + fill="freeze"/> +</svg> +</div> +<pre id="test"> +<script> +'use strict'; + +SimpleTest.waitForExplicitFinish(); + +window.addEventListener('load', () => { + const svg = document.getElementById('svg'); + is(getComputedStyle(svg).height, '0px', 'Computed height should be zero'); + SimpleTest.finish(); +}); +</script> +</pre> diff --git a/dom/smil/test/test_smilAnimateMotion.xhtml b/dom/smil/test/test_smilAnimateMotion.xhtml new file mode 100644 index 0000000000..68ac7fb96a --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotion.xhtml @@ -0,0 +1,51 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=436418 +--> +<head> + <title>Test for animateMotion behavior</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilAnimateMotion.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436418">Mozilla Bug 436418</a> +<p id="display"></p> +<div id="content" style="visibility: hidden"> + +<!-- NOTE: Setting font-size so we can test 'em' units --> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" style="font-size: 500px" + onload="this.pauseAnimations()"> + <!-- XXXdholbert Right now, 'em' conversions are correct if we set font-size + on rect using the inline style attr. However, if we use 'font-size' attr, + then 'em' units end up using the inherited font-size instead. Bug? --> + <rect x="20" y="20" width="200" height="200" style="font-size: 10px"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var timingData = new SMILTimingData(1.0, 6.0); + testBundleList(gMotionBundles, timingData); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml b/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml new file mode 100644 index 0000000000..c2d72e5435 --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotionInvalidValues.xhtml @@ -0,0 +1,176 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=436418 +--> +<head> + <title>Test for animateMotion acceptance of invalid values</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js" /> + <script type="text/javascript" src="smilAnimateMotionValueLists.js" /> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436418">Mozilla Bug 436418</a> +<p id="display"></p> +<div id="content" style="visibility: hidden"> +<svg xmlns="http://www.w3.org/2000/svg" id="svg" + width="200px" height="200px" + onload="this.pauseAnimations()"> + <rect id="rect" x="20" y="20" width="200" height="200"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +// Constant strings (& string-arrays) +const SVGNS = "http://www.w3.org/2000/svg"; +const XLINKNS = "http://www.w3.org/1999/xlink"; + +// Constant objects +const gSvg = document.getElementById("svg"); +const gRect = document.getElementById("rect"); +const gUnAnimatedCTM = gRect.getCTM(); + +SimpleTest.waitForExplicitFinish(); + +function createAnim() +{ + var anim = document.createElementNS(SVGNS, "animateMotion"); + anim.setAttribute("dur", "2s"); + return gRect.appendChild(anim); +} + +function removeElem(aElem) +{ + aElem.remove(); +} + +function testAttr(aAttrName, aAttrValueArray, aIsValid) +{ + var componentsToCheck; + + for (var i in aAttrValueArray) { + var curVal = aAttrValueArray[i]; + var anim = createAnim(); + anim.setAttribute(aAttrName, curVal); + if (aAttrName == "rotate") { + // Apply a diagonal translation (so rotate='auto' will have an effect) + // and just test the rotation matrix components + anim.setAttribute("values", "0 0; 50 50"); + componentsToCheck = CTMUtil.CTM_COMPONENTS_ROTATE; + } else { + // Apply a supplementary rotation to make sure that we don't apply it if + // our value is rejected. + anim.setAttribute("rotate", Math.PI/4); + componentsToCheck = CTMUtil.CTM_COMPONENTS_ALL; + if (aAttrName == "keyPoints") { + // Add three times so we can test a greater range of values for + // keyPoints + anim.setAttribute("values", "0 0; 25 25; 50 50"); + anim.setAttribute("keyTimes", "0; 0.5; 1"); + anim.setAttribute("calcMode", "discrete"); + } + } + + var curCTM = gRect.getCTM(); + if (aIsValid) { + var errMsg = "CTM should have changed when applying animateMotion " + + "with '" + aAttrName + "' set to valid value '" + curVal + "'"; + CTMUtil.assertCTMNotEqual(curCTM, gUnAnimatedCTM, componentsToCheck, + errMsg, false); + } else { + var errMsg = "CTM should not have changed when applying animateMotion " + + "with '" + aAttrName + "' set to invalid value '" + curVal + "'"; + CTMUtil.assertCTMEqual(curCTM, gUnAnimatedCTM, componentsToCheck, + errMsg, false); + } + removeElem(anim); + } +} + +function createPath(aPathDescription) +{ + var path = document.createElementNS(SVGNS, "path"); + path.setAttribute("d", aPathDescription); + path.setAttribute("id", "thePath"); + return gSvg.appendChild(path); +} + +function createMpath(aAnimElement) +{ + var mpath = document.createElementNS(SVGNS, "mpath"); + mpath.setAttributeNS(XLINKNS, "href", "#thePath"); + return aAnimElement.appendChild(mpath); +} + +function testMpathElem(aPathValueArray, aIsValid) +{ + for (var i in aPathValueArray) { + var curVal = aPathValueArray[i]; + var anim = createAnim(); + var mpath = createMpath(anim); + var path = createPath(curVal); + + // Apply a supplementary rotation to make sure that we don't apply it if + // our value is rejected. + anim.setAttribute("rotate", Math.PI/4); + componentsToCheck = CTMUtil.CTM_COMPONENTS_ALL; + + if (aIsValid) { + var errMsg = "CTM should have changed when applying animateMotion " + + "with mpath linking to a path with valid value '" + curVal + "'"; + + CTMUtil.assertCTMNotEqual(gRect.getCTM(), gUnAnimatedCTM, + componentsToCheck, errMsg, false); + } else { + var errMsg = "CTM should not have changed when applying animateMotion " + + "with mpath linking to a path with invalid value '" + curVal + "'"; + CTMUtil.assertCTMEqual(gRect.getCTM(), gUnAnimatedCTM, + componentsToCheck, errMsg, false); + } + removeElem(anim); + removeElem(path); + removeElem(mpath); + } +} + +// Main Function +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testAttr("values", gValidValues, true); + testAttr("values", gInvalidValues, false); + + testAttr("rotate", gValidRotate, true); + testAttr("rotate", gInvalidRotate, false); + + testAttr("to", gValidToBy, true); + testAttr("to", gInvalidToBy, false); + + testAttr("by", gValidToBy, true); + testAttr("by", gInvalidToBy, false); + + testAttr("path", gValidPath, true); + testAttr("path", gInvalidPath, false); + testAttr("path", gValidPathWithErrors, true); + + testAttr("keyPoints", gValidKeyPoints, true); + testAttr("keyPoints", gInvalidKeyPoints, false); + + testMpathElem(gValidPath, true); + testMpathElem(gInvalidPath, false); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml b/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml new file mode 100644 index 0000000000..3a147cd094 --- /dev/null +++ b/dom/smil/test/test_smilAnimateMotionOverrideRules.xhtml @@ -0,0 +1,215 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=436418 +--> +<head> + <title>Test for overriding of path-defining attributes for animateMotion</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js" /> + <script type="text/javascript" src="smilAnimateMotionValueLists.js" /> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=436418">Mozilla Bug 436418</a> +<p id="display"></p> +<div id="content" style="visibility: hidden"> +<svg xmlns="http://www.w3.org/2000/svg" id="svg" + width="200px" height="200px" + onload="this.pauseAnimations()"> + <!-- Paths for mpath to refer to --> + <path id="validPathElem" d="M10 10 h-10"/> + <path id="invalidPathElem" d="abc"/> + + <!-- The rect whose motion is animated --> + <rect id="rect" x="20" y="20" width="200" height="200"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +// Constant strings (& string-arrays) +const SVGNS = "http://www.w3.org/2000/svg"; +const XLINKNS = "http://www.w3.org/1999/xlink"; + +// Constant objects +const gSvg = document.getElementById("svg"); +const gRect = document.getElementById("rect"); +const gUnAnimatedCTM = gRect.getCTM(); + +// Values for path-defining attributes, and their expected +// CTMs halfway through the animation +var gMpathValidTarget = "#validPathElem"; +var gMpathCTM = CTMUtil.generateCTM([ 5, 10, 0 ]); + +var gMpathInvalidTargetA = "#invalidPathElem"; +var gMpathInvalidTargetB = "#nonExistentElem"; + +var gInvalidAttrValue = "i-am-invalid"; // Invalid for all tested attributes + +var gPathValidValue = "M20 20 h10"; +var gPathCTM = CTMUtil.generateCTM([ 25, 20, 0 ]); + +var gValuesValidValue = "30 30; 40 30" +var gValuesCTM = CTMUtil.generateCTM([ 35, 30, 0 ]); + +var gFromValidValue = "50 50"; + +var gByValidValue = "10 2"; +var gPureByCTM = CTMUtil.generateCTM([ 5, 1, 0 ]); +var gFromByCTM = CTMUtil.generateCTM([ 55, 51, 0 ]); + +var gToValidValue = "80 60"; +var gPureToCTM = CTMUtil.generateCTM([ 40, 30, 0 ]); +var gFromToCTM = CTMUtil.generateCTM([ 65, 55, 0 ]); + + +SimpleTest.waitForExplicitFinish(); + +function createAnim() +{ + var anim = document.createElementNS(SVGNS, "animateMotion"); + return gRect.appendChild(anim); +} + +function removeElem(aElem) +{ + aElem.remove(); +} + +function createMpath(aAnimElement, aHrefVal) +{ + var mpath = document.createElementNS(SVGNS, "mpath"); + mpath.setAttributeNS(XLINKNS, "href", aHrefVal); + return aAnimElement.appendChild(mpath); +} + +function runTest() { + // Start out with valid values for all path-defining attributes + var attrSettings = { + "mpath" : gMpathValidTarget, + "path" : gPathValidValue, + "values" : gValuesValidValue, + "from" : gFromValidValue, + "to" : gToValidValue, + "by" : gByValidValue, + }; + + // Test that <mpath> overrides everything below it + testAttrSettings(attrSettings, gMpathCTM, + "<mpath> should win"); + var mpathInvalidTargets = [gMpathInvalidTargetA, gMpathInvalidTargetB]; + for (var i in mpathInvalidTargets) { + var curInvalidValue = mpathInvalidTargets[i]; + attrSettings.mpath = curInvalidValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid <mpath> should block animation"); + } + delete attrSettings.mpath; + + // Test that 'path' overrides everything below it + testAttrSettings(attrSettings, gPathCTM, + "'path' should win vs all but mpath"); + attrSettings.path = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'path' should block animation vs all but mpath"); + delete attrSettings.path; + + // Test that 'values' overrides everything below it + testAttrSettings(attrSettings, gValuesCTM, + "'values' should win vs from/by/to"); + attrSettings.values = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'values' should block animation vs from/by/to"); + delete attrSettings.values; + + // Test that 'from' & 'to' overrides 'by' + testAttrSettings(attrSettings, gFromToCTM, + "'from/to' should win vs 'by'"); + attrSettings.to = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'to' should block animation vs 'by'"); + delete attrSettings.to; + + // Test that 'from' & 'by' are effective + testAttrSettings(attrSettings, gFromByCTM, + "'from/by' should be visible"); + attrSettings.by = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'by' should block animation"); + delete attrSettings.from; + + // REINSERT "to" & fix up "by" so we can test pure-"to" vs pure-"by" + attrSettings.to = gToValidValue; + attrSettings.by = gByValidValue; + testAttrSettings(attrSettings, gPureToCTM, + "pure-'to' should be effective & beat pure-'by'"); + attrSettings.to = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid pure-'to' should block animation vs pure-'by'"); + delete attrSettings.to; + + // Test that pure-"by" is effective + testAttrSettings(attrSettings, gPureByCTM, + "pure-by should be visible"); + attrSettings.by = gInvalidAttrValue; + testAttrSettings(attrSettings, gUnAnimatedCTM, + "invalid 'by' should block animation"); + delete attrSettings.by; + + // Make sure that our hash is empty now. + for (var unexpectedKey in attrSettings) { + ok(false, "Unexpected mapping remains in attrSettings: " + + unexpectedKey + "-->" + unexpectedValue); + } +} + +function testAttrSettings(aAttrValueHash, aExpectedCTM, aErrMsg) +{ + var isDebug = false; // XXdholbert + !isDebug || todo(false, "ENTERING testAttrSettings"); + // Set up animateMotion element + var animElement = document.createElementNS(SVGNS, "animateMotion"); + animElement.setAttribute("dur", "2s"); + for (var attrName in aAttrValueHash) { + !isDebug || todo(false, "setting '" + attrName +"' to '" + + aAttrValueHash[attrName] +"'"); + if (attrName == "mpath") { + createMpath(animElement, aAttrValueHash[attrName]); + } else { + animElement.setAttribute(attrName, aAttrValueHash[attrName]); + } + } + + gRect.appendChild(animElement); + + // Seek to halfway through animation + SMILUtil.getSVGRoot().setCurrentTime(1); // Seek halfway through animation + + // Check CTM against expected value + CTMUtil.assertCTMEqual(gRect.getCTM(), aExpectedCTM, + CTMUtil.CTM_COMPONENTS_ALL, aErrMsg, false); + + // CLEAN UP + SMILUtil.getSVGRoot().setCurrentTime(0); + removeElem(animElement); +} + +// Main Function +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + runTest(); + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilBackwardsSeeking.xhtml b/dom/smil/test/test_smilBackwardsSeeking.xhtml new file mode 100644 index 0000000000..20974e6de3 --- /dev/null +++ b/dom/smil/test/test_smilBackwardsSeeking.xhtml @@ -0,0 +1,191 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for backwards seeking behavior </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" /> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for backwards seeking behavior **/ + +var gSvg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Pause our document, so that the setCurrentTime calls are the only + // thing affecting document time + gSvg.pauseAnimations(); + + // We define a series of scenarios, sample times, and expected return values + // from getStartTime. + // + // Each scenario is basically a variation on the following arrangement: + // + // <svg> + // <set ... dur="1s" begin="<A-BEGIN>"/> + // <set ... dur="1s" begin="<B-BEGIN>"/> + // </svg> + // + // Each test then consists of the following: + // animA: attributes to be applied to a + // animB: attributes to be applied to b + // times: a series of triples which consist of: + // <sample time, a's expected start time, b's expected start time> + // * The sample time is the time passed to setCurrentTime and so is + // in seconds. + // * The expected start times are compared with the return value of + // getStartTime. To check for an unresolved start time where + // getStartTime would normally throw an exception use + // 'unresolved'. + // * We also allow the special notation to indicate a call to + // beginElement + // <'beginElementAt', id of animation element, offset> + // + // In the diagrams below '^' means the time before the seek and '*' is the + // seek time. + var testCases = Array(); + + // 0: Simple case + // + // A: +------- + // B: +------- begin: a.begin + // * ^ + testCases[0] = { + 'animA': {'begin':'1s', 'id':'a'}, + 'animB': {'begin':'a.begin'}, + 'times': [ [0, 1, 1], + [1, 1, 1], + [2, 'unresolved', 'unresolved'], + [0, 1, 1], + [1.5, 1, 1], + [1, 1, 1], + [2, 'unresolved', 'unresolved'] ] + }; + + // 1: Restored times should be live + // + // When we restore times they should be live. So we have the following + // scenario. + // + // A: +------- + // B: +------- begin: a.begin + // * ^ + // + // Then we call beginElement at an earlier time which should give us the + // following. + // + // A: +------- + // B: +------- + // * ^ + // + // If the times are not live however we'll end up with this + // + // A: +------- + // B: +-+------- + // * ^ + testCases[1] = { + 'animA': {'begin':'1s', 'id':'a', 'restart':'whenNotActive'}, + 'animB': {'begin':'a.begin', 'restart':'always'}, + 'times': [ [0, 1, 1], + [2, 'unresolved', 'unresolved'], + [0.25, 1, 1], + ['beginElementAt', 'a', 0.25], // = start time of 0.5 + [0.25, 0.5, 0.5], + [1, 0.5, 0.5], + [1.5, 'unresolved', 'unresolved'] ] + }; + + // 2: Multiple intervals A + // + // A: +- +- + // B: +- +- begin: a.begin+4s + // * ^ + testCases[2] = { + 'animA': {'begin':'1s; 3s', 'id':'a'}, + 'animB': {'begin':'a.begin+4s'}, + 'times': [ [0, 1, 5], + [3, 3, 5], + [6.5, 'unresolved', 7], + [4, 'unresolved', 5], + [6, 'unresolved', 7], + [2, 3, 5], + ['beginElementAt', 'a', 0], + [2, 2, 5], + [5, 'unresolved', 5], + [6, 'unresolved', 6], + [7, 'unresolved', 7], + [8, 'unresolved', 'unresolved'] ] + }; + + for (var i = 0; i < testCases.length; i++) { + gSvg.setCurrentTime(0); + var test = testCases[i]; + + // Create animation elements + var animA = createAnim(test.animA); + var animB = createAnim(test.animB); + + // Run samples + for (var j = 0; j < test.times.length; j++) { + var times = test.times[j]; + if (times[0] == 'beginElementAt') { + var anim = getElement(times[1]); + anim.beginElementAt(times[2]); + } else { + gSvg.setCurrentTime(times[0]); + checkStartTime(animA, times[1], times[0], i, 'a'); + checkStartTime(animB, times[2], times[0], i, 'b'); + } + } + + // Tidy up + animA.remove(); + animB.remove(); + } + + SimpleTest.finish(); +} + +function createAnim(attr) +{ + const svgns = "http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns, 'set'); + anim.setAttribute('attributeName','x'); + anim.setAttribute('to','10'); + anim.setAttribute('dur','1s'); + for (name in attr) { + anim.setAttribute(name, attr[name]); + } + return gSvg.appendChild(anim); +} + +function checkStartTime(anim, expectedStartTime, sampleTime, caseNum, id) +{ + var startTime = 'unresolved'; + try { + startTime = anim.getStartTime(); + } catch (e) { + if (e.name != "InvalidStateError" || + e.code != DOMException.INVALID_STATE_ERR) + throw e; + } + + var msg = "Test case " + caseNum + ", t=" + sampleTime + " animation '" + + id + "': Unexpected getStartTime:"; + is(startTime, expectedStartTime, msg); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSFontStretchRelative.xhtml b/dom/smil/test/test_smilCSSFontStretchRelative.xhtml new file mode 100644 index 0000000000..01988a881b --- /dev/null +++ b/dom/smil/test/test_smilCSSFontStretchRelative.xhtml @@ -0,0 +1,102 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg"> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/* This testcase verifies that animated values of "wider" and "narrower" for + "font-stretch" have the expected effect, across all possible inherited + values of the property. + XXXdholbert Currently, we don't support animating relative values of + font-stretch, so most of the tests here use todo_is() rather than is(). +*/ + +SimpleTest.waitForExplicitFinish(); + +const gPropertyName="font-stretch"; + +// List of non-relative font-stretch values, from smallest to largest +const gFontStretchValues = [ + ["ultra-condensed", "50%"], + ["extra-condensed", "62.5%"], + ["condensed", "75%"], + ["semi-condensed", "87.5%"], + ["normal", "100%"], + ["semi-expanded", "112.5%"], + ["expanded", "125%"], + ["extra-expanded", "150%"], + ["ultra-expanded", "200%"], +]; + +function testFontStretchValue([baseValue, computedValue], [narrowerStep, computedNarrowerStep], [widerStep, computedWiderStep]) +{ + var svg = SMILUtil.getSVGRoot(); + var gElem = document.createElementNS(SVG_NS, "g"); + gElem.setAttribute("style", "font-stretch: " + baseValue); + svg.appendChild(gElem); + + var textElem = document.createElementNS(SVG_NS, "text"); + gElem.appendChild(textElem); + + var animElem = document.createElementNS(SVG_NS, "set"); + animElem.setAttribute("attributeName", gPropertyName); + animElem.setAttribute("attributeType", "CSS"); + animElem.setAttribute("begin", "0s"); + animElem.setAttribute("dur", "indefinite"); + textElem.appendChild(animElem); + + // CHECK EFFECT OF 'narrower' + // NOTE: Using is() instead of todo_is() for ultra-condensed, since + // 'narrower' has no effect on that value. + var myIs = (baseValue == "ultra-condensed" ? is : todo_is); + animElem.setAttribute("to", "narrower"); + SMILUtil.getSVGRoot().setCurrentTime(1.0); // Force a resample + myIs(SMILUtil.getComputedStyleSimple(textElem, gPropertyName), computedNarrowerStep, + "checking effect of 'narrower' on inherited value '" + baseValue + "'"); + + // CHECK EFFECT OF 'wider' + // NOTE: using is() instead of todo_is() for ultra-expanded, since + // 'wider' has no effect on that value. + myIs = (baseValue == "ultra-expanded" ? is : todo_is); + animElem.setAttribute("to", "wider"); + SMILUtil.getSVGRoot().setCurrentTime(1.0); // Force a resample + myIs(SMILUtil.getComputedStyleSimple(textElem, gPropertyName), computedWiderStep, + "checking effect of 'wider' on inherited value '" + baseValue + "'"); + + // Removing animation should clear animated effects + textElem.removeChild(animElem); + svg.removeChild(gElem); +} + +function main() +{ + var valuesList = gFontStretchValues; + for (var baseIdx in valuesList) { + // 'narrower' and 'wider' are expected to shift us by one slot, but not + // past the ends of the list of possible values. + var narrowerIdx = Math.max(baseIdx - 1, 0); + var widerIdx = Math.min(baseIdx + 1, valuesList.length - 1); + + testFontStretchValue(valuesList[baseIdx], + valuesList[narrowerIdx], valuesList[widerIdx]); + } + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSFromBy.xhtml b/dom/smil/test/test_smilCSSFromBy.xhtml new file mode 100644 index 0000000000..586305fb8a --- /dev/null +++ b/dom/smil/test/test_smilCSSFromBy.xhtml @@ -0,0 +1,49 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSFromBy.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is + more predictable. (otherwise, line-height varies depending on platform) + --> + <text x="20" y="20" style="line-height: 10px !important">testing 123</text> + <line/> + <marker/> + <filter><feDiffuseLighting/></filter> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testBundleList(gFromByBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSFromTo.xhtml b/dom/smil/test/test_smilCSSFromTo.xhtml new file mode 100644 index 0000000000..501adbc4a8 --- /dev/null +++ b/dom/smil/test/test_smilCSSFromTo.xhtml @@ -0,0 +1,76 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSFromTo.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is + more predictable. (otherwise, line-height varies depending on platform) + --> + <text x="20" y="20" style="line-height: 10px !important">testing 123</text> + <line/> + <image/> + <marker/> + <clipPath><circle/></clipPath> + <filter><feFlood/></filter> + <filter><feDiffuseLighting/></filter> + <linearGradient><stop/></linearGradient> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function checkForUntestedProperties(bundleList) +{ + // Create the set of all the properties we know about + var propertySet = {}; + for (propertyLabel in gPropList) { + // insert property + propertySet[gPropList[propertyLabel].attrName] = null; + } + // Remove tested properties from the set + for (var bundleIdx in bundleList) { + var bundle = bundleList[bundleIdx]; + delete propertySet[bundle.animatedAttribute.attrName]; + } + // Warn about remaining (untested) properties + for (var untestedProp in propertySet) { + ok(false, "No tests for property '" + untestedProp + "'"); + } +} + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // FIRST: Warn about any properties that are missing tests + checkForUntestedProperties(gFromToBundles); + + // Run the actual tests + testBundleList(gFromToBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSInherit.xhtml b/dom/smil/test/test_smilCSSInherit.xhtml new file mode 100644 index 0000000000..4e262d3aa9 --- /dev/null +++ b/dom/smil/test/test_smilCSSInherit.xhtml @@ -0,0 +1,85 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="300px" height="200px" + onload="this.pauseAnimations()"> + <!-- At 50% through the animation, the following should be true: + * First <g> has font-size = 5px (1/2 between 0px and 10px) + * Next <g> has font-size = 10px (1/2 between inherit=5px and 15px) + * Next <g> has font-size = 15px (1/2 between inherit=10px and 20px) + * Next <g> has font-size = 20px (1/2 between inherit=15px and 25px) + * Next <g> has font-size = 25px (1/2 between inherit=20px and 30px) + * Next <g> has font-size = 30px (1/2 between inherit=25px and 35px) + * Next <g> has font-size = 35px (1/2 between inherit=30px and 40px) + * Next <g> has font-size = 40px (1/2 between inherit=35px and 45px) + * Next <g> has font-size = 45px (1/2 between inherit=40px and 50px) + * Next <g> has font-size = 50px (1/2 between inherit=45px and 55px) + * <text> has font-size = 75px (1/2 between inherit=50px and 100px) + --> + <g><animate attributeName="font-size" attributeType="CSS" + from="0px" to="10px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="15px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="20px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="25px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="30px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="35px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="40px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="45px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="50px" begin="0s" dur="1s"/> + <g><animate attributeName="font-size" attributeType="CSS" + from="inherit" to="55px" begin="0s" dur="1s"/> + <text y="100px" x="0px"> + abc + <animate attributeName="font-size" attributeType="CSS" + from="inherit" to="100px" begin="0s" dur="1s"/> + </text></g></g></g></g></g></g></g></g></g></g> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +function main() { + // Pause & seek to halfway through animation + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + svg.setCurrentTime(0.5); + + var text = document.getElementsByTagName("text")[0]; + var computedVal = SMILUtil.getComputedStyleSimple(text, "font-size"); + var expectedVal = "75px"; + + // NOTE: There's a very small chance (1/11! = 1/39,916,800) that we'll happen + // to composite our 11 animations in the correct order, in which cast this + // "todo_is" test would sporadically pass. I think this is infrequent enough + // to accept as a sporadic pass rate until this bug is fixed (at which point + // this "todo_is" will become an "is") + todo_is(computedVal, expectedVal, + "deeply-inherited font-size halfway through animation"); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSInvalidValues.xhtml b/dom/smil/test/test_smilCSSInvalidValues.xhtml new file mode 100644 index 0000000000..5e8e682af9 --- /dev/null +++ b/dom/smil/test/test_smilCSSInvalidValues.xhtml @@ -0,0 +1,59 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +var invalidTestcaseBundles = [ + new TestcaseBundle(gPropList.opacity, [ + new AnimTestcaseFromTo("", "", { noEffect: true }), + new AnimTestcaseFromTo("", "0.5", { noEffect: true }), + new AnimTestcaseFromTo(".", "0.5", { noEffect: true }), + new AnimTestcaseFromTo("0.5", "-", { noEffect: true }), + new AnimTestcaseFromTo("0.5", "bogus", { noEffect: true }), + new AnimTestcaseFromTo("bogus", "bogus", { noEffect: true }), + ]), + new TestcaseBundle(gPropList.color, [ + new AnimTestcaseFromTo("", "", { noEffect: true }), + new AnimTestcaseFromTo("", "red", { noEffect: true }), + new AnimTestcaseFromTo("greeeen", "red", { noEffect: true }), + new AnimTestcaseFromTo("rgb(red, 255, 255)", "red", { noEffect: true }), + new AnimTestcaseFromTo("#FFFFFFF", "red", { noEffect: true }), + new AnimTestcaseFromTo("bogus", "bogus", { noEffect: true }), + ]), +]; +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // Run the tests + testBundleList(invalidTestcaseBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCSSPaced.xhtml b/dom/smil/test/test_smilCSSPaced.xhtml new file mode 100644 index 0000000000..1f6b3509ae --- /dev/null +++ b/dom/smil/test/test_smilCSSPaced.xhtml @@ -0,0 +1,44 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSPaced.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <text x="20" y="20">testing 123</text> + <marker/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testBundleList(gPacedBundles, new SMILTimingData(1.0, 6.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilChangeAfterFrozen.xhtml b/dom/smil/test/test_smilChangeAfterFrozen.xhtml new file mode 100644 index 0000000000..97fe131678 --- /dev/null +++ b/dom/smil/test/test_smilChangeAfterFrozen.xhtml @@ -0,0 +1,571 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL when things change after an animation is frozen</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=533291">Mozilla Bug 533291</a> +<p id="display"></p> +<!-- Bug 628848: The following should be display: none but we currently don't + handle percentage lengths properly when the whole fragment is display: none + --> +<div id="content" style="visibility: hidden"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <g id="circleParent"> + <circle cx="0" cy="20" r="15" fill="blue" id="circle"/> + </g> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL values that are context-sensitive **/ + +/* See bugs 533291 and 562815. + + The format of each test is basically: + 1) create some animated and frozen state + 2) test the animated values + 3) change the context + 4) test that context-sensitive animation values have changed + + Ideally, after changing the context (3), the animated state would instantly + update. However, this is not currently the case for many situations. + + For CSS properties we have bug 545282 - In animations involving 'inherit' + / 'currentColor', changes to inherited value / 'color' don't show up in + animated value immediately + + For SVG lengths we have bug 508206 - Relative units used in + animation don't update immediately + + (There are a few of todo_is's in the following tests so that if those bugs + are ever resolved we'll know to update this test case accordingly.) + + So in between (3) and (4) we force a sample. This is currently done by + calling SVGSVGElement.setCurrentTime with the same current time which has the + side effect of forcing a sample. + + What we *are* testing is that we're not too zealous with caching animation + values whilst in the frozen state. Normally we'd say, "Hey, we're frozen, + let's just use the same animation result as last time" but for some + context-sensitive animation values that doesn't work. +*/ + +/* Global Variables */ +const SVGNS = "http://www.w3.org/2000/svg"; + +// Animation parameters -- not used for <set> animation +const ANIM_DUR = "4s"; +const TIME_ANIM_END = "4"; +const TIME_AFTER_ANIM_END = "5"; + +const gSvg = document.getElementById("svg"); +const gCircle = document.getElementById("circle"); +const gCircleParent = document.getElementById("circleParent"); + +SimpleTest.waitForExplicitFinish(); + +// MAIN FUNCTION +// ------------- + +function main() +{ + ok(gSvg.animationsPaused(), "should be paused by <svg> load handler"); + is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + const tests = + [ testBaseValueChange, + testCurrentColorChange, + testCurrentColorChangeUsingStyle, + testCurrentColorChangeOnFallback, + testInheritChange, + testInheritChangeUsingStyle, + testEmUnitChangeOnProp, + testEmUnitChangeOnPropBase, + testEmUnitChangeOnLength, + testPercentUnitChangeOnProp, + testPercentUnitChangeOnLength, + testRelativeFontSize, + testRelativeFontWeight, + testRelativeFont, + testCalcFontSize, + testDashArray, + testClip + ]; + + while (tests.length) { + tests.shift()(); + } + SimpleTest.finish(); +} + +// HELPER FUNCTIONS +// ---------------- +function createAnimSetTo(attrName, toVal) +{ + var anim = document.createElementNS(SVGNS,"set"); + anim.setAttribute("attributeName", attrName); + anim.setAttribute("to", toVal); + return gCircle.appendChild(anim); +} + +function createAnimBy(attrName, byVal) +{ + var anim = document.createElementNS(SVGNS,"animate"); + anim.setAttribute("attributeName", attrName); + anim.setAttribute("dur", ANIM_DUR); + anim.setAttribute("begin","0s"); + anim.setAttribute("by", byVal); + anim.setAttribute("fill", "freeze"); + return gCircle.appendChild(anim); +} + +function createAnimFromTo(attrName, fromVal, toVal) +{ + var anim = document.createElementNS(SVGNS,"animate"); + anim.setAttribute("attributeName", attrName); + anim.setAttribute("dur", ANIM_DUR); + anim.setAttribute("begin","0s"); + anim.setAttribute("from", fromVal); + anim.setAttribute("to", toVal); + anim.setAttribute("fill", "freeze"); + return gCircle.appendChild(anim); +} + +// Common setup code for each test function: seek to 0, and make sure +// the previous test cleaned up its animations. +function setupTest() { + gSvg.setCurrentTime(0); + if (gCircle.firstChild) { + ok(false, "Previous test didn't clean up after itself."); + } +} + +// THE TESTS +// --------- + +function testBaseValueChange() +{ + setupTest(); + var anim = createAnimBy("cx", "50"); + gSvg.setCurrentTime(TIME_ANIM_END); + is(gCircle.cx.animVal.value, 50, + "Checking animated cx as anim ends"); + + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(gCircle.cx.animVal.value, 50, + "Checking animated cx after anim ends"); + + gCircle.setAttribute("cx", 20); + is(gCircle.cx.animVal.value, 70, + "Checking animated cx after anim ends & after changing base val"); + + anim.remove(); // clean up +} + +function testCurrentColorChange() +{ + gCircle.setAttribute("color", "red"); // At first: currentColor=red + var anim = createAnimSetTo("fill", "currentColor"); + + gSvg.setCurrentTime(0); // trigger synchronous sample + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)", + "Checking animated fill=currentColor after animating"); + + gCircle.setAttribute("color", "lime"); // Change: currentColor=lime + // Bug 545282: We should really detect this change and update immediately but + // currently we don't until we get sampled again + todo_is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=currentColor after updating context but before " + + "sampling"); + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=currentColor after updating context"); + + // Clean up + gCircle.removeAttribute("color"); + gCircle.firstChild.remove(); +} + +function testCurrentColorChangeUsingStyle() +{ + setupTest(); + gCircle.setAttribute("style", "color: red"); // At first: currentColor=red + var anim = createAnimSetTo("fill", "currentColor"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)", + "Checking animated fill=currentColor after animating (using style attr)"); + + gCircle.setAttribute("style", "color: lime"); // Change: currentColor=lime + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=currentColor after updating context " + + "(using style attr)"); + + // Clean up + gCircle.removeAttribute("style"); + gCircle.firstChild.remove(); +} + +function getFallbackColor(pServerStr) +{ + return pServerStr.substr(pServerStr.indexOf(" ")+1); +} + +function testCurrentColorChangeOnFallback() +{ + setupTest(); + gCircle.setAttribute("color", "red"); // At first: currentColor=red + var anim = createAnimSetTo("fill", "url(#missingGrad) currentColor"); + + gSvg.setCurrentTime(0); + var fallback = + getFallbackColor(SMILUtil.getComputedStyleSimple(gCircle, "fill")); + is(fallback, "rgb(255, 0, 0)", + "Checking animated fallback fill=currentColor after animating"); + + gCircle.setAttribute("color", "lime"); // Change: currentColor=lime + gSvg.setCurrentTime(0); + fallback = getFallbackColor(SMILUtil.getComputedStyleSimple(gCircle, "fill")); + is(fallback, "rgb(0, 255, 0)", + "Checking animated fallback fill=currentColor after updating context"); + + gCircle.removeAttribute("style"); + gCircle.firstChild.remove(); +} + +function testInheritChange() +{ + setupTest(); + gCircleParent.setAttribute("fill", "red"); // At first: inherit=red + var anim = createAnimSetTo("fill", "inherit"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)", + "Checking animated fill=inherit after animating"); + + gCircleParent.setAttribute("fill", "lime"); // Change: inherit=lime + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=inherit after updating context"); + + gCircleParent.removeAttribute("fill"); + gCircle.firstChild.remove(); +} + +function testInheritChangeUsingStyle() +{ + setupTest(); + gCircleParent.setAttribute("style", "fill: red"); // At first: inherit=red + var anim = createAnimSetTo("fill", "inherit"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(255, 0, 0)", + "Checking animated fill=inherit after animating (using style attr)"); + + gCircleParent.setAttribute("style", "fill: lime"); // Change: inherit=lime + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "fill"), "rgb(0, 255, 0)", + "Checking animated fill=inherit after updating context " + + "(using style attr)"); + + gCircleParent.removeAttribute("style"); + gCircle.firstChild.remove(); +} + +function testEmUnitChangeOnProp() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("font-size", "2em"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "20px", + "Checking animated font-size=2em after animating ends"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "40px", + "Checking animated font-size=2em after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.firstChild.remove(); +} + +function testEmUnitChangeOnPropBase() +{ + // Test the case where the base value for our animation sandwich is + // context-sensitive. + // Currently, this is taken care of by the compositor which keeps a cached + // base value and compares it with the current base value. This test then just + // serves as a regression test in case the compositor's behaviour changes. + setupTest(); + gSvg.setAttribute("font-size", "10px"); // At first: font-size: 10px + gCircleParent.setAttribute("font-size", "1em"); // Base: 10px + var anim = createAnimBy("font-size", "10px"); + + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "20px", + "Checking animated font-size=20px after anim ends"); + + gSvg.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "30px", + "Checking animated font-size=30px after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.firstChild.remove(); +} + +function testEmUnitChangeOnLength() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("cx", "2em"); + + gSvg.setCurrentTime(0); + is(gCircle.cx.animVal.value, 20, + "Checking animated length=2em after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + // Bug 508206: We should really detect this change and update immediately but + // currently we don't until we get sampled again + todo_is(gCircle.cx.animVal.value, 40, + "Checking animated length=2em after updating context but before sampling"); + + gSvg.setCurrentTime(0); + is(gCircle.cx.animVal.value, 40, + "Checking animated length=2em after updating context and after " + + "resampling"); + + gCircleParent.removeAttribute("font-size"); + gCircle.firstChild.remove(); +} + +function testPercentUnitChangeOnProp() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("font-size", "150%"); + + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "15px", + "Checking animated font-size=150% after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + is(SMILUtil.getComputedStyleSimple(gCircle, "font-size"), "30px", + "Checking animated font-size=150% after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.firstChild.remove(); +} + +function testPercentUnitChangeOnLength() +{ + setupTest(); + var oldHeight = gSvg.getAttribute("height"); + gSvg.setAttribute("height", "100px"); // At first: viewport height: 100px + var anim = createAnimSetTo("cy", "100%"); + + gSvg.setCurrentTime(0); // Force synchronous sample so animation takes effect + // Due to bug 627594 (SVGLength.value for percent value lengths doesn't + // reflect updated viewport until reflow) the following will fail. + // Check that it does indeed fail so that when that bug is fixed this test + // can be updated. + todo_is(gCircle.cy.animVal.value, 100, + "Checking animated length=100% after animating but before reflow"); + // force a layout flush (Bug 627594) + gSvg.getCTM(); + // Even after doing a reflow though we'll still fail due to bug 508206 + // (Relative units used in animation don't update immediately) + todo_is(gCircle.cy.animVal.value, 100, + "Checking animated length=100% after animating but before resampling"); + gSvg.setCurrentTime(0); + // Now we should be up to date + is(gCircle.cy.animVal.value, 100, + "Checking animated length=100% after animating"); + + gSvg.setAttribute("height", "50px"); // Change: height: 50px + // force a layout flush (Bug 627594) + gSvg.getCTM(); + gSvg.setCurrentTime(0); // Bug 508206 + is(gCircle.cy.animVal.value, 50, + "Checking animated length=100% after updating context"); + + gSvg.setAttribute("height", oldHeight); + gCircle.firstChild.remove(); +} + +function testRelativeFontSize() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("font-size", "larger"); + + gSvg.setCurrentTime(0); + var fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + // CSS 2 suggests a scaling factor of 1.2 so we should be looking at something + // around about 12 or so + ok(fsize > 10 && fsize < 20, + "Checking animated font-size > 10px after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + ok(fsize > 20, "Checking animated font-size > 20px after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.firstChild.remove(); +} + +function testRelativeFontWeight() +{ + setupTest(); + gCircleParent.setAttribute("font-weight", "100"); // At first: font-weight 100 + var anim = createAnimSetTo("font-weight", "bolder"); + // CSS 2: 'bolder': Specifies the next weight that is assigned to a font + // that is darker than the inherited one. If there is no such weight, it + // simply results in the next darker numerical value (and the font remains + // unchanged), unless the inherited value was '900', in which case the + // resulting weight is also '900'. + + gSvg.setCurrentTime(0); + var weight = + parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-weight")); + ok(weight > 100, "Checking animated font-weight > 100 after animating"); + + gCircleParent.setAttribute("font-weight", "800"); // Change: font-weight 800 + gSvg.setCurrentTime(0); + weight = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-weight")); + is(weight, 900, + "Checking animated font-weight = 900 after updating context"); + + gCircleParent.removeAttribute("font-weight"); + gCircle.firstChild.remove(); +} + +function testRelativeFont() +{ + // Test a relative font-size as part of a 'font' spec since the code path + // is different in this case + // It turns out that, due to the way we store shorthand font properties, we + // don't need to worry about marking such values as context-sensitive since we + // seem to store them in their relative form. If, however, we change the way + // we store shorthand font properties in the future, this will serve as + // a useful regression test. + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + // We must be sure to set every part of the shorthand property to some + // non-context sensitive value because we want to test that even if only the + // font-size is relative we will update it appropriately. + var anim = + createAnimSetTo("font", "normal normal bold larger/normal sans-serif"); + + gSvg.setCurrentTime(0); + var fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + ok(fsize > 10 && fsize < 20, + "Checking size of shorthand 'font' > 10px after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + ok(fsize > 20, + "Checking size of shorthand 'font' > 20px after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.firstChild.remove(); +} + +function testCalcFontSize() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "10px"); // At first: font-size: 10px + var anim = createAnimSetTo("font-size", "calc(110% + 0.1em)"); + + gSvg.setCurrentTime(0); + var fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + // Font size should be 1.1 * 10px + 0.1 * 10px = 12 + is(fsize, 12, "Checking animated calc font-size == 12px after animating"); + + gCircleParent.setAttribute("font-size", "20px"); // Change: font-size: 20px + gSvg.setCurrentTime(0); + fsize = parseInt(SMILUtil.getComputedStyleSimple(gCircle, "font-size")); + is(fsize, 24, "Checking animated calc font-size == 24px after updating " + + "context"); + + gCircleParent.removeAttribute("font-size"); + gCircle.firstChild.remove(); +} + +function testDashArray() +{ + // stroke dasharrays don't currently convert units--but if someone ever fixes + // that, hopefully this test will fail and remind us not to cache percentage + // values in that case + setupTest(); + var oldHeight = gSvg.getAttribute("height"); + var oldWidth = gSvg.getAttribute("width"); + gSvg.setAttribute("height", "100px"); // At first: viewport: 100x100px + gSvg.setAttribute("width", "100px"); + var anim = createAnimFromTo("stroke-dasharray", "0 5", "0 50%"); + + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + + // Now we should be up to date + is(SMILUtil.getComputedStyleSimple(gCircle, "stroke-dasharray"), "0, 50%", + "Checking animated stroke-dasharray after animating"); + + gSvg.setAttribute("height", "50px"); // Change viewport: 50x50px + gSvg.setAttribute("width", "50px"); + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(gCircle, "stroke-dasharray"), "0, 50%", + "Checking animated stroke-dasharray after updating context"); + + gSvg.setAttribute("height", oldHeight); + gSvg.setAttribute("width", oldWidth); + gCircle.firstChild.remove(); +} + +function testClip() +{ + setupTest(); + gCircleParent.setAttribute("font-size", "20px"); // At first: font-size: 20px + + // The clip property only applies to elements that establish a new + // viewport so we need to create a nested svg and add animation to that + var nestedSVG = document.createElementNS(SVGNS, "svg"); + nestedSVG.setAttribute("clip", "rect(0px 0px 0px 0px)"); + gCircleParent.appendChild(nestedSVG); + + var anim = createAnimSetTo("clip", "rect(1em 1em 1em 1em)"); + // createAnimSetTo will make the animation a child of gCircle so we need to + // move it so it targets nestedSVG instead + nestedSVG.appendChild(anim); + + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(nestedSVG, "clip"), + "rect(20px, 20px, 20px, 20px)", + "Checking animated clip rect after animating"); + + gCircleParent.setAttribute("font-size", "10px"); // Change: font-size: 10px + gSvg.setCurrentTime(TIME_AFTER_ANIM_END); + is(SMILUtil.getComputedStyleSimple(nestedSVG, "clip"), + "rect(10px, 10px, 10px, 10px)", + "Checking animated clip rect after updating context"); + + gCircleParent.removeAttribute("font-size"); + gCircleParent.removeChild(nestedSVG); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilConditionalProcessing.html b/dom/smil/test/test_smilConditionalProcessing.html new file mode 100644 index 0000000000..302c445b6e --- /dev/null +++ b/dom/smil/test/test_smilConditionalProcessing.html @@ -0,0 +1,93 @@ +<!doctype html> +<html> +<head> + <meta charset="utf-8"> + <title>Test conditional processing tests applied to animations</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle r="50" fill="blue" id="circle"> + <set attributeName="cy" to="100" begin="0s" dur="100s" id="a"/> + <set attributeName="cx" to="100" begin="a.end" dur="100s" id="b"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + +SimpleTest.waitForExplicitFinish(); + +function run() { + SpecialPowers.pushPrefEnv({"set": [["intl.accept_languages", "en"]]}, runInternal); +} + +function runInternal() { + var svg = document.getElementById("svg"), + a = document.getElementById("a"), + b = document.getElementById("b"), + circle = document.getElementById("circle"); + + // Check initial state + svg.setCurrentTime(50); + is(a.getStartTime(), 0, "a has resolved start time at start"); + is(circle.cy.animVal.value, 100, "a is in effect at start"); + is(b.getStartTime(), 100, "b has resolved start time at start"); + + // Add a failing conditional processing test + a.setAttribute("systemLanguage", "no-such-language"); + ok(hasUnresolvedStartTime(a), + "a has unresolved start time with failing conditional processing test"); + is(circle.cy.animVal.value, 0, + "a is not in effect with failing conditional processing test"); + ok(hasUnresolvedStartTime(b), + "b has unresolved start time with failing conditional processing test on a"); + + // Remove failing conditional processing test + a.removeAttribute("systemLanguage"); + is(a.getStartTime(), 0, "a has resolved start time after removing test"); + is(circle.cy.animVal.value, 100, "a is in effect after removing test"); + is(b.getStartTime(), 100, "b has resolved start time after removing test on a"); + + // Add another failing conditional processing test + // According to the spec, if a null string or empty string value is set for + // the 'systemLanguage' attribute, the attribute returns "false". + a.setAttribute("systemLanguage", ""); + + // Fast forward until |a| would have finished + var endEventsReceived = 0; + a.addEventListener("endEvent", function() { endEventsReceived++; }); + svg.setCurrentTime(150); + is(endEventsReceived, 0, + "a does not dispatch end events with failing condition processing test"); + is(circle.cx.animVal.value, 0, + "b is not in effect with failing conditional processing test on a"); + + // Make test pass + a.setAttribute("systemLanguage", "en"); + is(circle.cx.animVal.value, 100, + "b is in effect with passing conditional processing test on a"); + + SimpleTest.finish(); +} + +function hasUnresolvedStartTime(anim) { + // getStartTime throws INVALID_STATE_ERR when there is no current interval + try { + anim.getStartTime(); + return false; + } catch (e) { + return true; + } +} + +window.addEventListener("load", run); + +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilContainerBinding.xhtml b/dom/smil/test/test_smilContainerBinding.xhtml new file mode 100644 index 0000000000..20e1013d4b --- /dev/null +++ b/dom/smil/test/test_smilContainerBinding.xhtml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for adding and removing animations from a time container</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-20" cy="20" r="15" fill="blue" id="circle"> + <set attributeName="cy" to="120" begin="0s; 2s" dur="1s" id="b"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for adding and removing animations from a time container **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = getElement("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // Create animation and check initial state + var anim = createAnim(); + anim.setAttribute('begin','b.begin+2s; 6s'); + ok(noStart(anim), "Animation has start time before attaching to document."); + + // Attach animation to container + var circle = getElement("circle"); + circle.appendChild(anim); + + // Check state after attaching + is(anim.getStartTime(), 2); + + // Unbind from tree -- the syncbase instance time(s) should become unresolved + // but the offset time should remain + anim.remove(); + is(anim.getStartTime(), 6); + + // Rebind and check everything is re-resolved + circle.appendChild(anim); + is(anim.getStartTime(), 2); + + // Advance document time to t=1s + // Now the current interval for b is 2s-3s but the current interval for anim + // is still 2s-2.5s based on b's previous interval + svg.setCurrentTime(1); + is(anim.getStartTime(), 2); + + // Unbind + anim.remove(); + is(anim.getStartTime(), 6); + + // Rebind + // At this point only the current interval will be re-added to anim (this is + // for consistency since old intervals may or may not have been filtered). + // Therefore the start time should be 4s instead of 2s. + circle.appendChild(anim); + is(anim.getStartTime(), 4); + + SimpleTest.finish(); +} + +function createAnim() { + const svgns="http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns,'set'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('to','100'); + anim.setAttribute('dur','0.5s'); + return anim; +} + +function noStart(elem) { + var exceptionCaught = false; + + try { + elem.getStartTime(); + } catch(e) { + exceptionCaught = true; + is (e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is (e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime."); + } + + return exceptionCaught; +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilCrossContainer.xhtml b/dom/smil/test/test_smilCrossContainer.xhtml new file mode 100644 index 0000000000..e4d30ef26d --- /dev/null +++ b/dom/smil/test/test_smilCrossContainer.xhtml @@ -0,0 +1,132 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for moving animations between time containers</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svga" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-20" cy="20" r="15" fill="blue" id="circlea"/> +</svg> +<svg id="svgb" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-20" cy="20" r="15" fill="blue" id="circleb"> + <set attributeName="cy" to="120" begin="4s" dur="1s" id="syncb"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for moving animations between time containers **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svga = getElement("svga"); + ok(svga.animationsPaused(), "should be paused by <svg> load handler"); + is(svga.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + svga.setCurrentTime(1); + + var svgb = getElement("svgb"); + ok(svgb.animationsPaused(), "should be paused by <svg> load handler"); + is(svgb.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + svgb.setCurrentTime(1); + + // Create animation and check initial state + var anim = createAnim(); + ok(noStart(anim), "Animation has start time before attaching to document"); + + // Attach animation to first container + var circlea = getElement("circlea"); + var circleb = getElement("circleb"); + circlea.appendChild(anim); + + // Check state after attaching + is(anim.getStartTime(), 2, + "Unexpected start time after attaching animation to target"); + is(circlea.cx.animVal.value, -20, + "Unexpected animated value for yet-to-start animation"); + is(circleb.cx.animVal.value, -20, + "Unexpected animated value for unanimated target"); + + // Move animation from first container to second + circleb.appendChild(anim); + + // Advance first container and check animation has no effect + svga.setCurrentTime(2); + is(anim.getStartTime(), 2, + "Unexpected start time after moving animation"); + is(circlea.cx.animVal.value, -20, + "Unexpected animated value for non-longer-animated target"); + is(circleb.cx.animVal.value, -20, + "Unexpected animated value for now yet-to-start animation"); + + // Advance second container and check the animation only affects it + svgb.setCurrentTime(2); + is(anim.getStartTime(), 2, "Start time changed after time container seek"); + is(circlea.cx.animVal.value, -20, + "Unanimated target changed after seek on other container"); + is(circleb.cx.animVal.value, 100, "Animated target not animated after seek"); + + // Remove animation so that it belongs to no container and check that + // advancing the second container to the next milestone doesn't cause a crash + // (when the animation controller goes to run the next milestone sample). + anim.remove(); + svgb.setCurrentTime(3); + + // Do likewise with syncbase relationships + + // Create the syncbase relationship + anim.setAttribute('begin', 'syncb.begin'); + + // Attach to second time container (where t=3s) + circleb.appendChild(anim); + is(anim.getStartTime(), 4, + "Unexpected start time for cross-time container syncbase dependency"); + + // Move to first time container (where t=1s). + // Because we're dealing with different time containers and both are paused, + // future times are effectively unresolved. + circlea.appendChild(anim); + ok(noStart(anim), "Unexpected start time for paused time container"); + + SimpleTest.finish(); +} + +function createAnim() { + const svgns="http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns,'set'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('to','100'); + anim.setAttribute('begin','2s'); + anim.setAttribute('dur','1s'); + return anim; +} + +function noStart(elem) { + var exceptionCaught = false; + + try { + elem.getStartTime(); + } catch(e) { + exceptionCaught = true; + is (e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is (e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime"); + } + + return exceptionCaught; +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml b/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml new file mode 100644 index 0000000000..10da496454 --- /dev/null +++ b/dom/smil/test/test_smilDynamicDelayedBeginElement.xhtml @@ -0,0 +1,103 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=699143 +--> +<head> + <title>Test for Bug 699143</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=699143">Mozilla Bug 699143</a> +<p id="display"></p> +<div id="content" style="display: none"> + <svg xmlns="http://www.w3.org/2000/svg"> + <rect id="r" height="500px" width="500px" fill="blue"/> + </svg> +</div> +<pre id="test"> +<script type="text/javascript"> +<![CDATA[ + +/** Test for Bug 699143 **/ +SimpleTest.waitForExplicitFinish(); + +// Values for 'width' attr on the <rect> above +const INITIAL_VAL = "500px" +const FROM_VAL = "20px"; +const TO_VAL = "80px"; + +// Helper functions + +// This function allows 10ms to pass +function allowTimeToPass() { + var initialDate = new Date(); + while (new Date() - initialDate < 10) {} +} + +// This function returns a newly created <animate> element for use in this test +function createAnim() { + var a = document.createElementNS('http://www.w3.org/2000/svg', 'animate'); + a.setAttribute('attributeName', 'width'); + a.setAttribute('from', FROM_VAL); + a.setAttribute('to', TO_VAL); + a.setAttribute('begin', 'indefinite'); + a.setAttribute('dur', '3s'); + a.setAttribute('fill', 'freeze'); + return a; +} + +// Main Functions +function main() { + // In unpatched Firefox builds, we'll only trigger Bug 699143 if we insert + // an animation and call beginElement() **after** the document start-time. + // Hence, we use executeSoon here to allow some time to pass. (And then + // we'll use a short busy-loop, for good measure.) + SimpleTest.executeSoon(runTest); +} + +function runTest() { + var svg = SMILUtil.getSVGRoot(); + + // In case our executeSoon fired immediately, we force a very small amount + // of time to pass here, using a 10ms busy-loop. + allowTimeToPass(); + + is(svg.getCurrentTime(), 0, + "even though we've allowed time to pass, we shouldn't have bothered " + + "updating the current time, since there aren't any animation elements"); + + // Insert an animation elem (should affect currentTime but not targeted attr) + var r = document.getElementById("r"); + var a = createAnim(); + r.appendChild(a); + isnot(svg.getCurrentTime(), 0, + "insertion of first animation element should have triggered a " + + "synchronous sample and updated our current time"); + is(r.width.animVal.valueAsString, INITIAL_VAL, + "inserted animation shouldn't have affected its targeted attribute, " + + "since it doesn't have any intervals yet"); + + // Trigger the animation & be sure it takes effect + a.beginElement(); + is(r.width.animVal.valueAsString, FROM_VAL, + "beginElement() should activate our animation & set its 'from' val"); + + // Rewind to time=0 & check target attr, to be sure beginElement()-generated + // interval starts later than that. + svg.setCurrentTime(0); + is(r.width.animVal.valueAsString, INITIAL_VAL, + "after rewinding to 0, our beginElement()-generated interval " + + "shouldn't be active yet"); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); + +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilExtDoc.xhtml b/dom/smil/test/test_smilExtDoc.xhtml new file mode 100644 index 0000000000..0323cbfb77 --- /dev/null +++ b/dom/smil/test/test_smilExtDoc.xhtml @@ -0,0 +1,80 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=628888 +--> +<head> + <title>Test for Bug 628888 - Animations in external document sometimes don't run</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> +</head> +<body style="margin:0px"> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=628888">Mozilla Bug 628888</a> +<p id="display"></p> +<div id="content" style="background: red; width: 50px; height: 50px"/> + +<pre id="test"> +<script type="application/javascript"> +<![CDATA[ + +/* Test for Bug 628888 - Animations in external document sometimes don't run + * + * This bug concerns a condition where an external document is loaded after the + * page show event is dispatched, leaving the external document paused. + * + * To reproduce the bug we attach an external document with animation after the + * page show event has fired. + * + * However, it is difficult to test if the animation is playing or not since we + * don't receive events from animations running in an external document. + * + * Our approach is to simply render the result to a canvas (which requires + * elevated privileges and that is why we are using a MochiTest rather + * than a reftest) and poll one of the pixels to see if it changes colour. + * + * This should mean the test succeeds quickly but fails slowly. + */ + +const POLL_INTERVAL = 100; // ms +const POLL_TIMEOUT = 10000; // ms +var accumulatedWaitTime = 0; + +function pageShow() +{ + var content = document.getElementById("content"); + content.style.filter = "url(smilExtDoc_helper.svg#filter)"; + window.setTimeout(checkResult, 0); +} + +function checkResult() +{ + var content = document.getElementById("content"); + var bbox = content.getBoundingClientRect(); + + var canvas = SpecialPowers.snapshotRect(window, bbox); + var ctx = canvas.getContext("2d"); + + var imgd = ctx.getImageData(bbox.width/2, bbox.height/2, 1, 1); + var isGreen = (imgd.data[0] == 0) && + (imgd.data[1] == 255) && + (imgd.data[2] == 0); + if (isGreen) { + ok(true, "Filter is animated as expected"); + } else if (accumulatedWaitTime >= POLL_TIMEOUT) { + ok(false, "No animation detected after waiting " + POLL_TIMEOUT + "ms"); + } else { + accumulatedWaitTime += POLL_INTERVAL; + window.setTimeout(checkResult, POLL_INTERVAL); + return; + } + // Hide our content since mochitests normally try to be visually "quiet" + content.style.display = 'none'; + SimpleTest.finish(); +} +window.addEventListener('pageshow', pageShow); +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilFillMode.xhtml b/dom/smil/test/test_smilFillMode.xhtml new file mode 100644 index 0000000000..ed270863bd --- /dev/null +++ b/dom/smil/test/test_smilFillMode.xhtml @@ -0,0 +1,86 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL fill modes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL fill modes **/ + +/* Global Variables */ +const svgns="http://www.w3.org/2000/svg"; +var svg = document.getElementById("svg"); +var circle = document.getElementById('circle'); + +SimpleTest.waitForExplicitFinish(); + +function createAnim() { + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('dur','4s'); + anim.setAttribute('begin','0s'); + anim.setAttribute('values', '10; 20'); + return circle.appendChild(anim); +} + +function main() { + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var tests = + [ testSetLaterA, + testSetLaterB, + testRemoveLater ]; + for (var i = 0; i < tests.length; i++) { + var anim = createAnim(); + svg.setCurrentTime(0); + tests[i](anim); + anim.remove(); + } + SimpleTest.finish(); +} + +function checkSample(time, expectedValue) { + svg.setCurrentTime(time); + is(circle.cx.animVal.value, expectedValue, + "Updated fill mode not applied to animation"); +} + +// Test that we can update the fill mode after an interval has played and it +// will be updated correctly. +function testSetLaterA(anim) { + checkSample(5, -100); + anim.setAttribute('fill', 'freeze'); + is(circle.cx.animVal.value, 20, + "Fill not applied for retrospectively set fill mode"); +} + +function testSetLaterB(anim) { + anim.setAttribute('fill', 'freeze'); + checkSample(5, 20); +} + +function testRemoveLater(anim) { + anim.setAttribute('fill', 'freeze'); + checkSample(5, 20); + anim.setAttribute('fill', 'remove'); + is(circle.cx.animVal.value, -100, + "Fill not removed for retrospectively set fill mode"); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilGetSimpleDuration.xhtml b/dom/smil/test/test_smilGetSimpleDuration.xhtml new file mode 100644 index 0000000000..a54139a898 --- /dev/null +++ b/dom/smil/test/test_smilGetSimpleDuration.xhtml @@ -0,0 +1,86 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for getSimpleDuration Behavior </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" + from="20" to="100" begin="1s" id="anim"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for getSimpleDuration Behavior **/ + +/* Global Variables */ +var svg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); + +function main() { + var anim = document.getElementById("anim"); + + /* Check initial state */ + checkForException(anim, "dur not set"); + + /* Check basic operation */ + anim.setAttribute("dur", "1s"); + is(anim.getSimpleDuration(), 1); + anim.setAttribute("dur", "1.5s"); + is(anim.getSimpleDuration(), 1.5); + + /* Check exceptional states */ + anim.setAttribute("dur", "0s"); + checkForException(anim, "dur=0s"); + anim.setAttribute("dur", "-1s"); + checkForException(anim, "dur=-1s"); + anim.setAttribute("dur", "indefinite"); + checkForException(anim, "dur=indefinite"); + anim.setAttribute("dur", "media"); + checkForException(anim, "dur=media"); + anim.setAttribute("dur", "abc"); + checkForException(anim, "dur=abc"); + anim.removeAttribute("dur"); + checkForException(anim, "dur not set"); + + /* Check range/syntax */ + anim.setAttribute("dur", "100ms"); + millisecondCompare(anim.getSimpleDuration(), 0.1); + anim.setAttribute("dur", "24h"); + is(anim.getSimpleDuration(), 60 * 60 * 24); + + SimpleTest.finish(); +} + +function millisecondCompare(a, b) { + is(Math.round(a * 1000), Math.round(b * 1000)); +} + +function checkForException(anim, descr) { + var gotException = false; + try { + var dur = anim.getSimpleDuration(); + } catch(e) { + is (e.name, "NotSupportedError", + "Wrong exception from getSimpleDuration"); + is (e.code, DOMException.NOT_SUPPORTED_ERR, + "Wrong exception from getSimpleDuration"); + gotException = true; + } + ok(gotException, + "Exception not thrown for indefinite simple duration when " + descr); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilGetStartTime.xhtml b/dom/smil/test/test_smilGetStartTime.xhtml new file mode 100644 index 0000000000..f854dd7cd9 --- /dev/null +++ b/dom/smil/test/test_smilGetStartTime.xhtml @@ -0,0 +1,103 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for getStartTime Behavior </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" + from="20" to="100" begin="indefinite" dur="1s" id="anim"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for getStartTime Behavior **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = document.getElementById("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var anim = document.getElementById("anim"); + // indefinite + var exceptionCaught = false; + try { + anim.getStartTime(); + } catch(e) { + exceptionCaught = true; + is(e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is(e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime."); + } + ok(exceptionCaught, "No exception thrown for indefinite start time."); + + // 1s + anim.setAttribute("begin", "1s"); + is(anim.getStartTime(), 1, "Unexpected start time with begin=1s"); + + // We have to be careful here when choosing a negative time that we choose + // a time that will create an interval that reaches past t=0 as SMIL has + // special rules for throwing away intervals that end before t=0 + anim.setAttribute("begin", "-0.5s"); + is(anim.getStartTime(), -0.5, "Unexpected start time with begin=-0.5s"); + + // Once the animation has begun, the begin time is fixed so we need to end the + // element (or advance the timeline) to override the previous start time + anim.endElement(); + + // However, now we have an end instance, and the SMIL model dictates that if + // we have end instances and no end event conditions and all end instances are + // before our next begin, there's no valid interval. To overcome this we add + // an indefinite end. + anim.setAttribute("end", "indefinite"); + + // Now test over the lifetime of the animation when there are multiple + // intervals + anim.setAttribute("begin", "1s; 3s"); + is(anim.getStartTime(), 1, "Unexpected start time before first interval"); + + svg.setCurrentTime(1); + is(anim.getStartTime(), 1, + "Unexpected start time at start of first interval"); + + svg.setCurrentTime(1.5); + is(anim.getStartTime(), 1, "Unexpected start time during first interval"); + + svg.setCurrentTime(2); + is(anim.getStartTime(), 3, "Unexpected start time after first interval"); + + svg.setCurrentTime(3); + is(anim.getStartTime(), 3, "Unexpected start time during second interval"); + + svg.setCurrentTime(4); + exceptionCaught = false; + try { + anim.getStartTime(); + } catch(e) { + exceptionCaught = true; + is(e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is(e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime."); + } + ok(exceptionCaught, "No exception thrown for in postactive state."); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilHyperlinking.xhtml b/dom/smil/test/test_smilHyperlinking.xhtml new file mode 100644 index 0000000000..49e9e7f7b0 --- /dev/null +++ b/dom/smil/test/test_smilHyperlinking.xhtml @@ -0,0 +1,229 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for hyperlinking</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display:none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL keySplines **/ + +/* Global Variables */ +const SVGNS="http://www.w3.org/2000/svg"; +var gSvg = document.getElementById("svg"); +var gAnim; + +var gTestStages = + [ testActive, + testSeekToFirst, + testKickStart, + testKickStartWithUnresolved, + testFiltering + ]; + +SimpleTest.waitForExplicitFinish(); + +function continueTest() +{ + if (!gTestStages.length) { + SimpleTest.finish(); + return; + } + + window.location.hash = ""; + if (gAnim) { + gAnim.remove(); + } + gAnim = createAnim(); + gSvg.setCurrentTime(0); + gTestStages.shift()(); +} + +function createAnim() { + var anim = document.createElementNS(SVGNS,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('from','0'); + anim.setAttribute('to','100'); + anim.setAttribute('dur','1s'); + anim.setAttribute('begin','indefinite'); + anim.setAttribute('id','anim'); + return document.getElementById('circle').appendChild(anim); +} + +// Traversing a hyperlink, condition 1: +// +// "If the target element is active, seek the document time back to the +// (current) begin time of the element. If there are multiple begin times, use +// the begin time that corresponds to the current "begin instance"." +// +function testActive() { + gAnim.setAttribute('begin','2s; 4s'); + gSvg.setCurrentTime(2.5); + fireLink(rewindActiveInterval1); +} + +function rewindActiveInterval1() { + is(gSvg.getCurrentTime(), 2, + "Unexpected time after activating link to animation in the middle of " + + "first active interval"); + + // Seek to second interval + gSvg.setCurrentTime(4.5); + fireLink(rewindActiveInterval2); +} + +function rewindActiveInterval2() { + is(gSvg.getCurrentTime(), 4, + "Unexpected time after activating link to animation in the middle of " + + "second active interval"); + + // Try a negative time + gAnim.setAttribute("begin", "-0.5"); + gSvg.setCurrentTime(0.2); + fireLink(rewindActiveIntervalAtZero); +} + +function rewindActiveIntervalAtZero() { + is(gSvg.getCurrentTime(), 0, + "Unexpected time after activating link to animation in the middle of " + + "an active interval that overlaps zero"); + + continueTest(); +} + +// Traversing a hyperlink, condition 2: +// +// "Else if the target element begin time is resolved (i.e., there is any +// resolved time in the list of begin times, or if the begin time was forced by +// an earlier hyperlink or a beginElement() method call), seek the document time +// (forward or back, as needed) to the earliest resolved begin time of the +// target element. Note that the begin time may be resolved as a result of an +// earlier hyperlink, DOM or event activation. Once the begin time is resolved, +// hyperlink traversal always seeks." +// +function testSeekToFirst() { + // Seek forwards + gAnim.setAttribute('begin','2s'); + gSvg.setCurrentTime(0); + fireLink(forwardToInterval1); +} + +function forwardToInterval1() { + is(gSvg.getCurrentTime(), 2, + "Unexpected time after activating link to animation scheduled to start " + + "the future"); + + // Seek backwards + gSvg.setCurrentTime(3.5); + fireLink(backwardToInterval1); +} + +function backwardToInterval1() { + is(gSvg.getCurrentTime(), 2, + "Unexpected time after activating link to animation that ran in the past"); + + // What if the first begin instance is negative? + gAnim.setAttribute('begin','-0.5s'); + gSvg.setCurrentTime(1); + fireLink(backwardToZero); +} + +function backwardToZero() { + is(gSvg.getCurrentTime(), 0, + "Unexpected time after activating link to animation that ran in the " + + "past with a negative time"); + + continueTest(); +} + +// Traversing a hyperlink, condition 3: +// +// "Else (animation begin time is unresolved) just resolve the target animation +// begin time at current document time. Disregard the sync-base or event base of +// the animation, and do not "back-propagate" any timing logic to resolve the +// child, but rather treat it as though it were defined with begin="indefinite" +// and just resolve begin time to the current document time." +// +function testKickStart() { + gSvg.setCurrentTime(1); + fireLink(startedAt1s); +} + +function startedAt1s() { + is(gSvg.getCurrentTime(), 1, + "Unexpected time after kick-starting animation with indefinite start " + + "by hyperlink"); + is(gAnim.getStartTime(), 1, + "Unexpected start time for kick-started animation"); + + continueTest(); +} + +function testKickStartWithUnresolved() { + gAnim.setAttribute("begin", "circle.click"); + gSvg.setCurrentTime(3); + fireLink(startedAt3s); +} + +function startedAt3s() { + is(gSvg.getCurrentTime(), 3, + "Unexpected time after kick-starting animation with unresolved start " + + "by hyperlink"); + is(gAnim.getStartTime(), 3, + "Unexpected start time for kick-started animation with unresolved begin " + + "condition"); + + continueTest(); +} + +function testFiltering() { + gAnim.setAttribute('begin','-3s; 1s; 2s; 3s; 4s; 5s; 6s; 7s; 8s; 9s; 10s'); + gSvg.setCurrentTime(12); + fireLink(rewindToFirst); +} + +function rewindToFirst() { + is(gSvg.getCurrentTime(), 1, + "Unexpected time after triggering animation with a hyperlink after " + + "numerous intervals have passed"); + + continueTest(); +} + +function fireLink(callback) { + // First we need to reset the hash because otherwise the redundant hashchange + // events will be suppressed + if (window.location.hash === '') { + fireLinkPart2(callback); + } else { + window.location.hash = ''; + window.addEventListener("hashchange", + function() { + window.setTimeout(fireLinkPart2, 0, callback); + }, {once: true}); + } +} + +function fireLinkPart2(callback) { + window.addEventListener("hashchange", + function() { + window.setTimeout(callback, 0); + }, {once: true}); + window.location.hash = '#anim'; +} + +window.addEventListener("load", continueTest); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilInvalidValues.html b/dom/smil/test/test_smilInvalidValues.html new file mode 100644 index 0000000000..d6a262fc8d --- /dev/null +++ b/dom/smil/test/test_smilInvalidValues.html @@ -0,0 +1,113 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=941315 +--> +<head> + <meta charset="utf-8"> + <title>Test invalid values cause the model to be updated (bug 941315)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=941315">Mozilla Bug 941315</a> +<p id="display"></p> +<div id="content" style="display: none"> +<svg width="100%" height="1" onload="this.pauseAnimations()"> + <rect> + <animate id="a" dur="100s"/> + <animate id="b" dur="5s" begin="a.end"/> + </rect> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + var a = $('a'), + b = $('b'); + + // Animation doesn't start until onload + SimpleTest.waitForExplicitFinish(); + window.addEventListener("load", runTests); + + // Make testing getStartTime easier + SVGAnimationElement.prototype.safeGetStartTime = function() { + try { + return this.getStartTime(); + } catch(e) { + if (e.name == "InvalidStateError" && + e.code == DOMException.INVALID_STATE_ERR) { + return 'none'; + } else { + ok(false, "Unexpected exception: " + e); + return null; + } + } + }; + + function runTests() { + [testSimpleDuration, testMin, testMax, testRepeatDur, testRepeatCount] + .forEach(function(test) { + is(b.getStartTime(), 100, "initial state before running " + test.name); + test(); + is(b.getStartTime(), 100, "final state after running " + test.name); + }); + SimpleTest.finish(); + } + + function testSimpleDuration() { + // Verify a valid value updates as expected + a.setAttribute("dur", "50s"); + is(b.safeGetStartTime(), 50, "valid simple duration"); + + // Check an invalid value also causes the model to be updated + a.setAttribute("dur", "abc"); // -> indefinite + is(b.safeGetStartTime(), "none", "invalid simple duration"); + + // Restore state + a.setAttribute("dur", "100s"); + } + + function testMin() { + a.setAttribute("min", "200s"); + is(b.safeGetStartTime(), 200, "valid min duration"); + + a.setAttribute("min", "abc"); // -> indefinite + is(b.safeGetStartTime(), 100, "invalid min duration"); + + a.removeAttribute("min"); + } + + function testMax() { + a.setAttribute("max", "50s"); + is(b.safeGetStartTime(), 50, "valid max duration"); + + a.setAttribute("max", "abc"); // -> indefinite + is(b.safeGetStartTime(), 100, "invalid max duration"); + + a.removeAttribute("max"); + } + + function testRepeatDur() { + a.setAttribute("repeatDur", "200s"); + is(b.safeGetStartTime(), 200, "valid repeatDur duration"); + + a.setAttribute("repeatDur", "abc"); // -> indefinite + is(b.safeGetStartTime(), 100, "invalid repeatDur duration"); + + a.removeAttribute("repeatDur"); + } + + function testRepeatCount() { + a.setAttribute("repeatCount", "2"); + is(b.safeGetStartTime(), 200, "valid repeatCount duration"); + + a.setAttribute("repeatCount", "abc"); // -> indefinite + is(b.safeGetStartTime(), 100, "invalid repeatCount duration"); + + a.removeAttribute("repeatCount"); + } +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilKeySplines.xhtml b/dom/smil/test/test_smilKeySplines.xhtml new file mode 100644 index 0000000000..c12f6f1e09 --- /dev/null +++ b/dom/smil/test/test_smilKeySplines.xhtml @@ -0,0 +1,296 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL keySplines</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL keySplines **/ + +/* Global Variables */ +const svgns="http://www.w3.org/2000/svg"; +var svg = document.getElementById("svg"); +var circle = document.getElementById('circle'); + +SimpleTest.waitForExplicitFinish(); + +function createAnim() { + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','0s'); + anim.setAttribute('fill', 'freeze'); + return circle.appendChild(anim); +} + +function main() { + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var tests = + [ testSimpleA, // these first four are from SVG 1.1 + testSimpleB, + testSimpleC, + testSimpleD, + testSimpleE, // bug 501569 + testMultipleIntervalsA, + testMultipleIntervalsB, + testMultipleIntervalsC, + testOneValue, + testFromTo, + testWrongNumSplines, + testToAnimation, + testOkSyntax, + testBadSyntaxA, + testBadSyntaxB + ]; + for (var i = 0; i < tests.length; i++) { + var anim = createAnim(); + svg.setCurrentTime(0); + tests[i](anim); + anim.remove(); + } + SimpleTest.finish(); +} + +function checkSample(time, expectedValue) { + svg.setCurrentTime(time); + is(circle.cx.animVal.value, expectedValue); +} + +function checkSampleRough(time, expectedValue, precision) { + const defaultPrecision = 0.00001; + if (typeof precision == "undefined") { + precision = defaultPrecision; + } + svg.setCurrentTime(time); + var diff = Math.abs(expectedValue - circle.cx.animVal.value); + ok(diff <= precision, + "Unexpected sample value got " + circle.cx.animVal.value + + ", expected " + expectedValue + " [error is " + diff + + ", tolerance is " + precision + "]"); +} + +/* + * These first four tests are the examples given in SVG 1.1, section 19.2.7 + */ + +function testSimpleA(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 0 1 1'); + checkSample(0, 10); + checkSample(1, 12.5); + checkSample(2, 15); + checkSample(3, 17.5); + checkSample(4, 20); +} + +function testSimpleB(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '.5 0 .5 1'); + checkSample(0, 10); + checkSampleRough(1, 11.058925); + checkSample(2, 15); + checkSampleRough(3, 18.941075); + checkSample(4, 20); +} + +function testSimpleC(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 .75 .25 1'); + checkSample(0, 10); + checkSampleRough(1, 18.101832); + checkSampleRough(2, 19.413430); + checkSampleRough(3, 19.886504); + checkSample(4, 20); +} + +function testSimpleD(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '1 0 .25 .25'); + checkSample(0, 10); + checkSampleRough(1, 10.076925); + checkSampleRough(2, 10.644369); + checkSampleRough(3, 16.908699); + checkSample(4, 20); +} + +// Bug 501569 -- SMILKeySpline(1, 0, 0, 1) miscalculates values just under 0.5 +function testSimpleE(anim) { + anim.setAttribute('dur','10s'); + anim.setAttribute('values', '0; 10'); + anim.setAttribute('keyTimes', '0; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '1 0 0 1'); + checkSample(0, 0); + checkSampleRough(0.001, 0); + checkSampleRough(4.95, 3.409174); + checkSampleRough(4.98, 3.819443); + checkSampleRough(4.99, 4.060174); + checkSampleRough(4.999, 4.562510); + checkSample(5, 5); + checkSampleRough(5.001, 5.437490); + checkSampleRough(5.01, 5.939826); + checkSampleRough(5.015, 6.075002); + checkSampleRough(5.02, 6.180557); + checkSampleRough(9.9999, 10); + checkSample(10, 10); +} + +function testMultipleIntervalsA(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20; 30'); + anim.setAttribute('keyTimes', '0; 0.25; 1'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 0 1 1; .5 0 .5 1;'); + checkSample(0.5, 15); + checkSampleRough(0.999, 20, 0.02); + checkSample(1, 20); + checkSampleRough(1.001, 20, 0.05); + checkSample(2.5, 25); + checkSampleRough(3.25, 29, 0.1); +} + +function testMultipleIntervalsB(anim) { + // as above but without keyTimes + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20; 30'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 0 1 1; .5 0 .5 1;'); + checkSample(1, 15); + checkSampleRough(1.999, 20, 0.01); + checkSample(2, 20); + checkSampleRough(2.001, 20, 0.01); + checkSample(3, 25); + checkSampleRough(3.5, 29, 0.1); +} + +function testMultipleIntervalsC(anim) { + // test some unusual (but valid) syntax + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20; 30'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', ' 0 .75 0.25 1 ; 1, 0 ,.25 .25 \t'); + checkSampleRough(3.5, 26.9, 0.2); +} + +function testOneValue(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '5'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '0 0 1 1'); + checkSample(0, 5); + checkSample(1.5, 5); +} + +function testFromTo(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('from', '10'); + anim.setAttribute('to', '20'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '.5 0 .5 1'); + checkSample(0, 10); + checkSampleRough(1, 11, 0.1); +} + +function testWrongNumSplines(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('from', '10'); + anim.setAttribute('to', '20'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '.5 0 .5 1; 0 0 1 1'); + // animation is in error, should do nothing + checkSample(1.5, -100); +} + +function testToAnimation(anim) { + anim.setAttribute('dur','4s'); + anim.setAttribute('to', '20'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', '.5 0 .5 1'); + checkSample(0, -100); + checkSampleRough(1, -87.3, 0.1); +} + +function testOkSyntax(anim) { + var okStrs = ['0,0,0,0', // all commas + ' 0 0 , 0 ,0 ', // mix of separators + '0 0 0 0;', // trailing semi-colon + '0 0 0 0 ;']; // " " + + for (var i = 0; i < okStrs.length; i++) { + testAnim = createAnim(); + testAnim.setAttribute('dur','4s'); + testAnim.setAttribute('from', '0'); + testAnim.setAttribute('to', '20'); + testAnim.setAttribute('calcMode', 'spline'); + testAnim.setAttribute('keySplines', okStrs[i]); + checkSample(4, 20); + testAnim.remove(); + } +} + +function testBadSyntaxA(anim) { + var badStrs = ['', // empty + ' ', // whitespace only + '0,1.1,0,0', // bad range + '0,0,0,-0.1', // " " + ' 0 0 , 0 0 ,', // stray comma + '1-1 0 0', // path-style separators + '0 0 0', // wrong number of values + '0 0 0 0 0', // " " + '0 0 0 0 0 0 0 0', // " " + '0 0 0; 0 0 0 0', // " " + '0 0 0; 0', // mis-placed semi-colon + ';0 0 0 0']; // " " + + for (var i = 0; i < badStrs.length; i++) { + testAnim = createAnim(); + testAnim.setAttribute('dur','4s'); + testAnim.setAttribute('from', '0'); + testAnim.setAttribute('to', '20'); + testAnim.setAttribute('calcMode', 'spline'); + testAnim.setAttribute('keySplines', badStrs[i]); + checkSample(4, -100); + testAnim.remove(); + } +} + +function testBadSyntaxB(anim) { + // test some illegal syntax + anim.setAttribute('dur','4s'); + anim.setAttribute('values', '10; 20; 30'); + anim.setAttribute('calcMode', 'spline'); + anim.setAttribute('keySplines', ' 0 .75 0.25 1 ; 1, A0 ,.25 .25 \t'); + // animation is in error, should do nothing + checkSample(3.5, -100); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilKeyTimes.xhtml b/dom/smil/test/test_smilKeyTimes.xhtml new file mode 100644 index 0000000000..43d3c91895 --- /dev/null +++ b/dom/smil/test/test_smilKeyTimes.xhtml @@ -0,0 +1,391 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL keyTimes</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=557885">Mozilla Bug + 557885</a> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL keyTimes **/ + +var gSvg = document.getElementById("svg"); +SimpleTest.waitForExplicitFinish(); + +function main() +{ + gSvg.pauseAnimations(); + + var testCases = Array(); + + // Simple case + testCases.push({ + 'attr' : { 'values': '0; 50; 100', + 'keyTimes': '0; .8; 1' }, + 'times': [ [ 4, 25 ], + [ 8, 50 ], + [ 9, 75 ], + [ 10, 100 ] ] + }); + + // Parsing tests + testCases.push(parseOk(' 0 ; .8;1 ')); // extra whitespace + testCases.push(parseNotOk(';0; .8; 1')); // leading semi-colon + testCases.push(parseNotOk('; .8; 1')); // leading semi-colon + testCases.push(parseOk('0; .8; 1;')); // trailing semi-colon + testCases.push(parseNotOk('')); // empty string + testCases.push(parseNotOk(' ')); // empty string + testCases.push(parseNotOk('0; .8')); // too few values + testCases.push(parseNotOk('0; .8; .9; 1')); // too many values + testCases.push(parseNotOk('0; 1; .8')); // non-increasing + testCases.push(parseNotOk('0; .8; .9')); // final value non-1 with + // calcMode=linear + testCases.push(parseOk('0; .8; .9', { 'calcMode': 'discrete' })); + testCases.push(parseNotOk('0.01; .8; 1')); // first value not 0 + testCases.push(parseNotOk('0.01; .8; 1', { 'calcMode': 'discrete' })); + // first value not 0 + testCases.push(parseNotOk('0; .8; 1.1')); // out of range + testCases.push(parseNotOk('-0.1; .8; 1')); // out of range + + + // 2 values + testCases.push({ + 'attr' : { 'values': '0; 50', + 'keyTimes': '0; 1' }, + 'times': [ [ 6, 30 ] ] + }); + + // 1 value + testCases.push({ + 'attr' : { 'values': '50', + 'keyTimes': ' 0' }, + 'times': [ [ 7, 50 ] ] + }); + + // 1 bad value + testCases.push({ + 'attr' : { 'values': '50', + 'keyTimes': '0.1' }, + 'times': [ [ 0, -100 ] ] + }); + + // 1 value, calcMode=discrete + testCases.push({ + 'attr' : { 'values': '50', + 'calcMode': 'discrete', + 'keyTimes': ' 0' }, + 'times': [ [ 7, 50 ] ] + }); + + // 1 bad value, calcMode=discrete + testCases.push({ + 'attr' : { 'values': '50', + 'calcMode': 'discrete', + 'keyTimes': '0.1' }, + 'times': [ [ 0, -100 ] ] + }); + + // from-to + testCases.push({ + 'attr' : { 'from': '10', + 'to': '20', + 'keyTimes': '0.0; 1.0' }, + 'times': [ [ 3.5, 13.5 ] ] + }); + + // from-to calcMode=discrete + testCases.push({ + 'attr' : { 'from': '10', + 'to': '20', + 'calcMode': 'discrete', + 'keyTimes': '0.0; 0.7' }, + 'times': [ [ 0, 10 ], + [ 6.9, 10 ], + [ 7.0, 20 ], + [ 10.0, 20 ], + [ 11.0, 20 ] ] + }); + + // from-to calcMode=discrete one keyTime only + testCases.push({ + 'attr' : { 'values': '20', + 'calcMode': 'discrete', + 'keyTimes': '0' }, + 'times': [ [ 0, 20 ], + [ 6.9, 20 ], + [ 7.0, 20 ], + [ 10.0, 20 ], + [ 11.0, 20 ] ] + }); + + // from-to calcMode=discrete one keyTime, mismatches no. values + testCases.push({ + 'attr' : { 'values': '10; 20', + 'calcMode': 'discrete', + 'keyTimes': '0' }, + 'times': [ [ 0, -100 ] ] + }); + + // to + testCases.push({ + 'attr' : { 'to': '100', + 'keyTimes': '0.0; 1.0' }, + 'times': [ [ 0, -100 ], + [ 7, 40 ] ] + }); + + // to -- bad number of keyTimes (too many) + testCases.push({ + 'attr' : { 'to': '100', + 'keyTimes': '0.0; 0.5; 1.0' }, + 'times': [ [ 2, -100 ] ] + }); + + // unfrozen to calcMode=discrete two keyTimes + testCases.push({ + 'attr' : { 'to': '100', + 'calcMode': 'discrete', + 'keyTimes': '0.0; 1.0', + 'fill': 'remove' }, + 'times': [ [ 0, -100 ], + [ 7, -100 ], + [ 10, -100 ], + [ 12, -100 ]] + }); + + // frozen to calcMode=discrete two keyTimes + testCases.push({ + 'attr' : { 'to': '100', + 'calcMode': 'discrete', + 'keyTimes': '0.0; 1.0' }, + 'times': [ [ 0, -100 ], + [ 7, -100 ], + [ 10, 100 ], + [ 12, 100 ] ] + }); + + // to calcMode=discrete -- bad number of keyTimes (one, expecting two) + testCases.push({ + 'attr' : { 'to': '100', + 'calcMode': 'discrete', + 'keyTimes': '0' }, + 'times': [ [ 0, -100 ], + [ 7, -100 ] ] + }); + + // values calcMode=discrete + testCases.push({ + 'attr' : { 'values': '0; 10; 20; 30', + 'calcMode': 'discrete', + 'keyTimes': '0;.2;.4;.6' }, + 'times': [ [ 0, 0 ], + [ 1.9, 0 ], + [ 2, 10 ], + [ 3.9, 10 ], + [ 4.0, 20 ], + [ 5.9, 20 ], + [ 6.0, 30 ], + [ 9.9, 30 ], + [ 10.0, 30 ] ] + }); + + // The following two accumulate tests are from SMIL 3.0 + // (Note that this behaviour differs from that defined for SVG Tiny 1.2 which + // specifically excludes the last value: "Note that in the case of discrete + // animation, the frozen value that is used is the value of the animation just + // before the end of the active duration.") + // accumulate=none + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'calcMode': 'discrete', + 'keyTimes': '0;.5;1', + 'fill': 'freeze', + 'repeatCount': '2', + 'accumulate': 'none' }, + 'times': [ [ 0, 0 ], + [ 5, 10 ], + [ 10, 0 ], + [ 15, 10 ], + [ 20, 20 ], + [ 25, 20 ] ] + }); + + // accumulate=sum + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'calcMode': 'discrete', + 'keyTimes': '0;.5;1', + 'fill': 'freeze', + 'repeatCount': '2', + 'accumulate': 'sum' }, + 'times': [ [ 0, 0 ], + [ 5, 10 ], + [ 10, 20 ], + [ 15, 30 ], + [ 20, 40 ], + [ 25, 40 ] ] + }); + + // If the interpolation mode is paced, the keyTimes attribute is ignored. + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'calcMode': 'paced', + 'keyTimes': '0;.2;1' }, + 'times': [ [ 0, 0 ], + [ 2, 4 ], + [ 5, 10 ] ] + }); + + // SMIL 3 has: + // If the simple duration is indefinite and the interpolation mode is + // linear or spline, any keyTimes specification will be ignored. + // However, since keyTimes represent "a proportional offset into the simple + // duration of the animation element" surely discrete animation too cannot use + // keyTimes when the simple duration is indefinite. Hence SVGT 1.2 is surely + // more correct when it has: + // If the simple duration is indefinite, any 'keyTimes' specification will + // be ignored. + // (linear) + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'dur': 'indefinite', + 'keyTimes': '0;.2;1' }, + 'times': [ [ 0, 0 ], + [ 5, 0 ] ] + }); + // (spline) + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'dur': 'indefinite', + 'calcMode': 'spline', + 'keyTimes': '0;.2;1', + 'keySplines': '0 0 1 1; 0 0 1 1' }, + 'times': [ [ 0, 0 ], + [ 5, 0 ] ] + }); + // (discrete) + testCases.push({ + 'attr' : { 'values': '0; 10; 20', + 'dur': 'indefinite', + 'calcMode': 'discrete', + 'keyTimes': '0;.2;1' }, + 'times': [ [ 0, 0 ], + [ 5, 0 ] ] + }); + + for (var i = 0; i < testCases.length; i++) { + gSvg.setCurrentTime(0); + var test = testCases[i]; + + // Create animation elements + var anim = createAnim(test.attr); + + // Run samples + for (var j = 0; j < test.times.length; j++) { + var times = test.times[j]; + gSvg.setCurrentTime(times[0]); + checkSample(anim, times[1], times[0], i); + } + + anim.remove(); + } + + // fallback to discrete for non-additive animation + var attr = { 'values': 'butt; round; square', + 'attributeName': 'stroke-linecap', + 'calcMode': 'linear', + 'keyTimes': '0;.2;1', + 'fill': 'remove' }; + var anim = createAnim(attr); + var samples = [ [ 0, 'butt' ], + [ 1.9, 'butt' ], + [ 2.0, 'round' ], + [ 9.9, 'round' ], + [ 10, 'butt' ] // fill=remove so we'll never set it to square + ]; + for (var i = 0; i < samples.length; i++) { + var sample = samples[i]; + gSvg.setCurrentTime(sample[0]); + checkLineCapSample(anim, sample[1], sample[0], + "[non-interpolatable fallback]"); + } + anim.remove(); + + SimpleTest.finish(); +} + +function parseOk(str, extra) +{ + var attr = { 'values': '0; 50; 100', + 'keyTimes': str }; + if (typeof(extra) == "object") { + for (name in extra) { + attr[name] = extra[name]; + } + } + return { + 'attr' : attr, + 'times': [ [ 0, 0 ] ] + }; +} + +function parseNotOk(str, extra) +{ + var result = parseOk(str, extra); + result.times = [ [ 0, -100 ] ]; + return result; +} + +function createAnim(attr) +{ + const svgns = "http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns, 'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','0s'); + anim.setAttribute('fill','freeze'); + for (name in attr) { + anim.setAttribute(name, attr[name]); + } + return document.getElementById('circle').appendChild(anim); +} + +function checkSample(anim, expectedValue, sampleTime, caseNum) +{ + var msg = "Test case " + caseNum + + " (keyTimes: '" + anim.getAttribute('keyTimes') + "'" + + " calcMode: " + anim.getAttribute('calcMode') + "), " + + "t=" + sampleTime + + ": Unexpected sample value:"; + is(anim.targetElement.cx.animVal.value, expectedValue, msg); +} + +function checkLineCapSample(anim, expectedValue, sampleTime, caseDescr) +{ + var msg = "Test case " + caseDescr + + " (keyTimes: '" + anim.getAttribute('keyTimes') + "'" + + " calcMode: " + anim.getAttribute('calcMode') + "), " + + "t=" + sampleTime + + ": Unexpected sample value:"; + var actualValue = + window.getComputedStyle(anim.targetElement). + getPropertyValue('stroke-linecap'); + is(actualValue, expectedValue, msg); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilKeyTimesPacedMode.xhtml b/dom/smil/test/test_smilKeyTimesPacedMode.xhtml new file mode 100644 index 0000000000..f2d6571fa8 --- /dev/null +++ b/dom/smil/test/test_smilKeyTimesPacedMode.xhtml @@ -0,0 +1,123 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Tests updated intervals</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=555026">Mozilla Bug 555026</a> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle r="10" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test that we ignore keyTimes attr when calcMode="paced" **/ + +/* Global Variables */ +const SVGNS = "http://www.w3.org/2000/svg"; +const ANIM_DUR = "2s"; +const HALF_TIME = "1"; +const ATTR_NAME = "cx" +const KEYTIMES_TO_TEST = [ + // potentially-valid values (depending on number of values in animation) + "0; 0.2; 1", + "0; 0.5", + "0; 1", + // invalid values: + "", "abc", "-0.5", "0; 0.5; 1.01", "5" +]; +const gSvg = document.getElementById("svg"); +const gCircle = document.getElementById("circle"); + +SimpleTest.waitForExplicitFinish(); + + +// MAIN FUNCTIONS +function main() { + ok(gSvg.animationsPaused(), "should be paused by <svg> load handler"); + is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testByAnimation(); + testToAnimation(); + testValuesAnimation(); + SimpleTest.finish(); +} + +function testByAnimation() { + for (var i = 0; i < KEYTIMES_TO_TEST.length; i++) { + setupTest(); + var anim = createAnim(); + anim.setAttribute("by", "200"); + var curKeyTimes = KEYTIMES_TO_TEST[i]; + anim.setAttribute("keyTimes", curKeyTimes); + + gSvg.setCurrentTime(HALF_TIME); + is(gCircle.cx.animVal.value, 100, + "Checking animVal with 'by' and keyTimes='" + curKeyTimes + "'"); + + anim.remove(); // clean up + } +} + +function testToAnimation() { + for (var i = 0; i < KEYTIMES_TO_TEST.length; i++) { + setupTest(); + var anim = createAnim(); + anim.setAttribute("to", "200"); + var curKeyTimes = KEYTIMES_TO_TEST[i]; + anim.setAttribute("keyTimes", curKeyTimes); + + gSvg.setCurrentTime(HALF_TIME); + is(gCircle.cx.animVal.value, 100, + "Checking animVal with 'to' and keyTimes='" + curKeyTimes + "'"); + + anim.remove(); // clean up + } +} + +function testValuesAnimation() { + for (var i = 0; i < KEYTIMES_TO_TEST.length; i++) { + setupTest(); + var anim = createAnim(); + anim.setAttribute("values", "100; 110; 200"); + var curKeyTimes = KEYTIMES_TO_TEST[i]; + anim.setAttribute("keyTimes", curKeyTimes); + + gSvg.setCurrentTime(HALF_TIME); + is(gCircle.cx.animVal.value, 150, + "Checking animVal with 'values' and keyTimes='" + curKeyTimes + "'"); + + anim.remove(); // clean up + } +} + +// HELPER FUNCTIONS +// Common setup code for each test function: seek to 0, and make sure +// the previous test cleaned up its animations. +function setupTest() { + gSvg.setCurrentTime(0); + if (gCircle.firstChild) { + ok(false, "Previous test didn't clean up after itself."); + } +} + +function createAnim() { + var anim = document.createElementNS(SVGNS,"animate"); + anim.setAttribute("attributeName", ATTR_NAME); + anim.setAttribute("dur", ANIM_DUR); + anim.setAttribute("begin", "0s"); + anim.setAttribute("calcMode", "paced"); + return gCircle.appendChild(anim); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilMappedAttrFromBy.xhtml b/dom/smil/test/test_smilMappedAttrFromBy.xhtml new file mode 100644 index 0000000000..5b70b5b83f --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrFromBy.xhtml @@ -0,0 +1,51 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilMappedAttrList.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSFromBy.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is + more predictable. (otherwise, line-height varies depending on platform) + --> + <text x="20" y="20" style="line-height: 10px !important">testing 123</text> + <line/> + <marker/> + <filter><feDiffuseLighting/></filter> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var testBundles = convertCSSBundlesToMappedAttr(gFromByBundles); + testBundleList(testBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilMappedAttrFromTo.xhtml b/dom/smil/test/test_smilMappedAttrFromTo.xhtml new file mode 100644 index 0000000000..57ef83800e --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrFromTo.xhtml @@ -0,0 +1,79 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilMappedAttrList.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSFromTo.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <!-- NOTE: hard-wiring 'line-height' so that computed value of 'font' is + more predictable. (otherwise, line-height varies depending on platform) + --> + <text x="20" y="20">testing 123</text> + <line/> + <image/> + <marker/> + <clipPath><circle/></clipPath> + <filter><feFlood/></filter> + <filter><feDiffuseLighting/></filter> + <linearGradient><stop/></linearGradient> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function checkForUntestedAttributes(bundleList) +{ + // Create the set of all the attributes we know about + var attributeSet = {}; + for (attributeLabel in gMappedAttrList) { + // insert attribute + attributeSet[gMappedAttrList[attributeLabel].attrName] = null; + } + // Remove tested properties from the set + for (var bundleIdx in bundleList) { + var bundle = bundleList[bundleIdx]; + delete attributeSet[bundle.animatedAttribute.attrName]; + } + // Warn about remaining (untested) properties + for (var untestedProp in attributeSet) { + ok(false, "No tests for attribute '" + untestedProp + "'"); + } +} + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var testBundles = convertCSSBundlesToMappedAttr(gFromToBundles); + + // FIRST: Warn about any attributes that are missing tests + checkForUntestedAttributes(testBundles); + + // Run the actual tests + testBundleList(testBundles, new SMILTimingData(1.0, 1.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilMappedAttrPaced.xhtml b/dom/smil/test/test_smilMappedAttrPaced.xhtml new file mode 100644 index 0000000000..81adea6390 --- /dev/null +++ b/dom/smil/test/test_smilMappedAttrPaced.xhtml @@ -0,0 +1,46 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for Animation Behavior on CSS Properties</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <script type="text/javascript" src="db_smilMappedAttrList.js"></script> + <script type="text/javascript" src="db_smilCSSPropertyList.js"></script> + <script type="text/javascript" src="db_smilCSSPaced.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg xmlns="http://www.w3.org/2000/svg" + width="200px" height="200px" font-size="50px" style="color: rgb(50,50,50)" + onload="this.pauseAnimations()"> + <rect x="20" y="20" width="200" height="200"/> + <text x="20" y="20">testing 123</text> + <marker/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ + +SimpleTest.waitForExplicitFinish(); + +function main() +{ + // Start out with document paused + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var testBundles = convertCSSBundlesToMappedAttr(gPacedBundles); + testBundleList(testBundles, new SMILTimingData(1.0, 6.0)); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilMinTiming.html b/dom/smil/test/test_smilMinTiming.html new file mode 100644 index 0000000000..f0bdd42502 --- /dev/null +++ b/dom/smil/test/test_smilMinTiming.html @@ -0,0 +1,93 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=948245 +--> +<head> + <meta charset="utf-8"> + <title>Test for Bug 948245</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=948245">Mozilla Bug 948245</a> +<p id="display"></p> +<div id="content"> +<svg id="svg" onload="this.pauseAnimations()"> + <rect fill="red" id="rect" x="0"> + <animate attributeName="x" to="100" id="animation" dur="100s" min="200s"/> + </rect> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + // The 'min' attribute introduces a kind of additional state into the SMIL + // model. If the 'min' attribute extends the active duration, the additional + // time between the amount of time the animation normally runs for (called the + // 'repeat duration') and the extended active duration is filled using the + // fill mode. + // + // Below we refer to this period of time between the end of the repeat + // duration and the end of the active duration as the 'extended period'. + // + // This test verifies that as we jump in and out of these states we produce + // the correct values. + // + // The test animation above produces an active interval that is longer than + // the 'repeating duration' of the animation. + var rect = $('rect'), + animation = $('animation'); + + // Animation doesn't start until onload + SimpleTest.waitForExplicitFinish(); + window.addEventListener("load", runTests); + + function runTests() { + ok($('svg').animationsPaused(), "should be paused by <svg> load handler"); + + // In the extended period (t=150s) we should not be animating or filling + // since the default fill mode is "none". + animation.ownerSVGElement.setCurrentTime(150); + is(rect.x.animVal.value, 0, + "Shouldn't fill in extended period with fill='none'"); + + // If we set the fill mode we should start filling. + animation.setAttribute("fill", "freeze"); + is(rect.x.animVal.value, 100, + "Should fill in extended period with fill='freeze'"); + + // If we unset the fill attribute we should stop filling. + animation.removeAttribute("fill"); + is(rect.x.animVal.value, 0, "Shouldn't fill after unsetting fill"); + + // If we jump back into the repeated interval (at t=50s) we should be + // animating. + animation.ownerSVGElement.setCurrentTime(50); + is(rect.x.animVal.value, 50, "Should be active in repeating interval"); + + // If we jump to the boundary at the start of the extended period we should + // not be filling (since we removed the fill attribute above). + animation.ownerSVGElement.setCurrentTime(100); + is(rect.x.animVal.value, 0, + "Shouldn't fill after seeking to boundary of extended period"); + + // If we apply a fill mode at this boundary point we should do regular fill + // behavior of using the last value in the interpolation range. + animation.setAttribute("fill", "freeze"); + is(rect.x.animVal.value, 100, + "Should fill at boundary to extended period"); + + // Check that if we seek past the interval we fill with the value at the end + // of the _repeat_duration_ not the value at the end of the + // _active_duration_. + animation.setAttribute("repeatCount", "1.5"); + animation.ownerSVGElement.setCurrentTime(225); + is(rect.x.animVal.value, 50, + "Should fill with the end of the repeat duration value"); + + SimpleTest.finish(); + } +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilRepeatDuration.html b/dom/smil/test/test_smilRepeatDuration.html new file mode 100644 index 0000000000..b746bb45d4 --- /dev/null +++ b/dom/smil/test/test_smilRepeatDuration.html @@ -0,0 +1,139 @@ +<!doctype html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=948245 +--> +<head> + <meta charset="utf-8"> + <title>Test for repeat duration calculation (Bug 948245)</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" +href="https://bugzilla.mozilla.org/show_bug.cgi?id=948245">Mozilla Bug 948245</a> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" onload="this.pauseAnimations()"> + <rect> + <animate id="a"/> + <animate id="b" begin="a.end"/> + </rect> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> + // Tests the calculation of the repeat duration which is one of the steps + // towards determining the active duration. + // + // The repeat duration is determined by the following three attributes: + // + // dur: may be definite (e.g. '2s') or 'indefinite' (the default) + // repeatCount: may be definite (e.g. '2.5'), 'indefinite', or not set + // repeatDur: may be definite (e.g. '5s'), 'indefinite', or not set + // + // That leaves 18 combinations to test. + var testCases = + [ + // 1. repeatDur: definite, repeatCount: definite, dur: definite + // (Two test cases here to ensure we get the minimum) + { repeatDur: 15, repeatCount: 2, dur: 10, result: 15 }, + { repeatDur: 25, repeatCount: 2, dur: 10, result: 20 }, + // 2. repeatDur: indefinite, repeatCount: definite, dur: definite + { repeatDur: 'indefinite', repeatCount: 2, dur: 10, result: 20 }, + // 3. repeatDur: not set, repeatCount: definite, dur: definite + { repeatCount: 2, dur: 10, result: 20 }, + // 4. repeatDur: definite, repeatCount: indefinite, dur: definite + { repeatDur: 15, repeatCount: 'indefinite', dur: 10, result: 15 }, + // 5. repeatDur: indefinite, repeatCount: indefinite, dur: definite + { repeatDur: 'indefinite', repeatCount: 'indefinite', dur: 10, + result: 'indefinite' }, + // 6. repeatDur: not set, repeatCount: indefinite, dur: definite + { repeatCount: 'indefinite', dur: 10, result: 'indefinite' }, + // 7. repeatDur: definite, repeatCount: not set, dur: definite + { repeatDur: 15, dur: 10, result: 15 }, + // 8. repeatDur: indefinite, repeatCount: not set, dur: definite + { repeatDur: 'indefinite', dur: 10, result: 'indefinite' }, + // 9. repeatDur: not set, repeatCount: not set, dur: definite + { dur: 10, result: 10 }, + // 10. repeatDur: definite, repeatCount: definite, dur: indefinite + { repeatDur: 15, repeatCount: 2, dur: 'indefinite', result: 15 }, + // 11. repeatDur: indefinite, repeatCount: definite, dur: indefinite + { repeatDur: 'indefinite', repeatCount: 2, dur: 'indefinite', + result: 'indefinite' }, + // 12. repeatDur: not set, repeatCount: definite, dur: indefinite + { repeatCount: 2, dur: 'indefinite', result: 'indefinite' }, + // 13. repeatDur: definite, repeatCount: indefinite, dur: indefinite + { repeatDur: 15, repeatCount: 'indefinite', dur: 'indefinite', + result: 15 }, + // 14. repeatDur: indefinite, repeatCount: indefinite, dur: indefinite + { repeatDur: 'indefinite', repeatCount: 'indefinite', dur: 'indefinite', + result: 'indefinite' }, + // 15. repeatDur: not set, repeatCount: indefinite, dur: indefinite + { repeatCount: 'indefinite', dur: 'indefinite', result: 'indefinite' }, + // 16. repeatDur: definite, repeatCount: not set, dur: indefinite + { repeatDur: 15, dur: 'indefinite', result: 15 }, + // 17. repeatDur: indefinite, repeatCount: not set, dur: indefinite + { repeatDur: 'indefinite', dur: 'indefinite', result: 'indefinite' }, + // 18. repeatDur: not set, repeatCount: not set, dur: indefinite + { dur: 'indefinite', result: 'indefinite' } + ]; + + // We can test the repeat duration by setting these attributes on animation + // 'a' and checking the start time of 'b' which is defined to start when 'a' + // finishes. + // + // Since 'a' has no end/min/max attributes the end of its active interval + // should coincide with the end of its repeat duration. + // + // Sometimes the repeat duration is defined to be 'indefinite'. In this case + // calling getStartTime on b will throw an exception so we need to catch that + // exception and translate it to 'indefinite' as follows: + function getRepeatDuration() { + try { + return $('b').getStartTime(); + } catch(e) { + if (e.name == "InvalidStateError" && + e.code == DOMException.INVALID_STATE_ERR) { + return 'indefinite'; + } else { + ok(false, "Unexpected exception: " + e); + return null; + } + } + } + + // Animation doesn't start until onload + SimpleTest.waitForExplicitFinish(); + window.addEventListener("load", runTests); + + // Run through each of the test cases + function runTests() { + ok($('svg').animationsPaused(), "should be paused by <svg> load handler"); + + testCases.forEach(function(test) { + var a = $('a'); + + // Set the attributes + var msgPieces = []; + [ 'repeatDur', 'repeatCount', 'dur' ].forEach(function(attr) { + if (typeof test[attr] != "undefined") { + a.setAttribute(attr, test[attr].toString()); + msgPieces.push(attr + ': ' + test[attr].toString()); + } else { + a.removeAttribute(attr); + msgPieces.push(attr + ': <not set>'); + } + }); + var msg = msgPieces.join(', '); + + // Check the result + is(getRepeatDuration(), test.result, msg); + }); + + SimpleTest.finish(); + } +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilRepeatTiming.xhtml b/dom/smil/test/test_smilRepeatTiming.xhtml new file mode 100644 index 0000000000..ace63d37b0 --- /dev/null +++ b/dom/smil/test/test_smilRepeatTiming.xhtml @@ -0,0 +1,96 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=485157 +--> +<head> + <title>Test repeat timing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=485157">Mozilla Bug + 485157</a> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="100px" height="100px"> + <rect width="100" height="100" fill="green"> + <set attributeName="width" to="100" dur="20s" repeatCount="5" begin="0s" + id="a" onrepeat="startWaiting(evt)"/> + <set attributeName="fill" attributeType="CSS" to="green" + begin="a.repeat(1)" onbegin="expectedBegin()" dur="20s"/> + <set attributeName="x" to="100" + begin="a.repeat(2)" onbegin="unexpectedBegin(this)" dur="20s"/> + <set attributeName="y" to="100" + begin="a.repeat(0)" onbegin="unexpectedBegin(this)" dur="20s"/> + <set attributeName="width" to="100" + begin="a.repeat(-1)" onbegin="unexpectedBegin(this)" dur="20s"/> + <set attributeName="height" to="100" + begin="a.repeat(a)" onbegin="unexpectedBegin(this)" dur="20s"/> + </rect> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test SMIL repeat timing **/ + +/* Global Variables */ +const gTimeoutDur = 5000; // Time until we give up waiting for events in ms +var gSvg = document.getElementById('svg'); +var gRect = document.getElementById('circle'); +var gTimeoutID; +var gGotBegin = false; + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +function testBegin() +{ + gSvg.setCurrentTime(19.999); +} + +function startWaiting(evt) +{ + is(evt.detail, 1, "Unexpected repeat event received: test broken"); + if (gGotBegin) + return; + + gTimeoutID = setTimeout(timeoutFail, gTimeoutDur); +} + +function timeoutFail() +{ + ok(false, "Timed out waiting for begin event"); + finish(); +} + +function expectedBegin() +{ + is(gGotBegin, false, + "Got begin event more than once for non-repeating animation"); + gGotBegin = true; + clearTimeout(gTimeoutID); + // Wait a moment before finishing in case there are erroneous events waiting + // to be processed. + setTimeout(finish, 10); +} + +function unexpectedBegin(elem) +{ + ok(false, "Got unexpected begin from animation with spec: " + + elem.getAttribute('begin')); +} + +function finish() +{ + gSvg.pauseAnimations(); + SimpleTest.finish(); +} + +window.addEventListener("load", testBegin); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilReset.xhtml b/dom/smil/test/test_smilReset.xhtml new file mode 100644 index 0000000000..a53f73c112 --- /dev/null +++ b/dom/smil/test/test_smilReset.xhtml @@ -0,0 +1,82 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Tests for SMIL Reset Behavior </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" from="20" to="100" begin="2s" dur="4s" + id="anim1" attributeType="XML"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Tests for SMIL Reset Behavior **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = document.getElementById("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var anim = document.getElementById("anim1"); + is(anim.getStartTime(), 2, "Unexpected initial start time"); + + svg.setCurrentTime(1); + anim.beginElementAt(2); + + // We now have two instance times: 2, 3 + + // Restart (and reset) animation at t=1 + anim.beginElement(); + + // Instance times should now be 1, 2 (3 should have be reset) + is(anim.getStartTime(), 1, + "Unexpected start time after restart. Perhaps the added instance time " + + "was cleared"); + svg.setCurrentTime(4); + // Instance times will now be 2 (1 will have be reset when we restarted) + is(anim.getStartTime(), 2, "Unexpected start time after seek"); + + // Create a two new instance times at t=4, 5 + anim.beginElement(); + anim.beginElementAt(1); + is(anim.getStartTime(), 4, "Unexpected start time after beginElement"); + + // Here is a white box test to make sure we don't discard instance times + // created by DOM calls when setting/unsetting the 'begin' spec + anim.removeAttribute('begin'); + is(anim.getStartTime(), 4, "Unexpected start time after clearing begin spec"); + svg.setCurrentTime(6); + is(anim.getStartTime(), 5, + "Second DOM instance time cleared when begin spec was removed"); + + // And likewise, when we set it again + anim.beginElementAt(1); // Instance times now t=5s, 7s + anim.setAttribute('begin', '1s'); // + t=1s + is(anim.getStartTime(), 5, "Unexpected start time after setting begin spec"); + svg.setCurrentTime(8); + is(anim.getStartTime(), 7, + "Second DOM instance time cleared when begin spec was added"); + + // But check we do update state appropriately + anim.setAttribute('begin', '8s'); + is(anim.getStartTime(), 8, "Interval not updated with updated begin spec"); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilRestart.xhtml b/dom/smil/test/test_smilRestart.xhtml new file mode 100644 index 0000000000..3e03dfdbcc --- /dev/null +++ b/dom/smil/test/test_smilRestart.xhtml @@ -0,0 +1,102 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL Restart Behavior </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <!-- These 3 circles only differ in their animation's "restart" value --> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" from="20" to="100" begin="1s" dur="4s" + restart="always" id="always" attributeType="XML"/> + </circle> + <circle cx="20" cy="60" r="15" fill="blue"> + <animate attributeName="cx" from="20" to="100" begin="1s" dur="4s" + restart="whenNotActive" id="whenNotActive" attributeType="XML"/> + </circle> + <circle cx="20" cy="100" r="15" fill="blue"> + <animate attributeName="cx" from="20" to="100" begin="1s" dur="4s" + restart="never" id="never" attributeType="XML"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL Restart Behavior **/ + +/* Global Variables */ +var svg = document.getElementById("svg"); +var always = document.getElementById("always"); +var whenNotActive = document.getElementById("whenNotActive"); +var never = document.getElementById("never"); + +SimpleTest.waitForExplicitFinish(); + +function tryRestart(elem, state, expected) { + var restartTime = svg.getCurrentTime(); + elem.beginElement(); + var restart = false; + try { + restart = (elem.getStartTime() === restartTime); + } catch (e) { + if (e.name != "InvalidStateError" || + e.code != DOMException.INVALID_STATE_ERR) + throw e; + restart = false; + } + if (expected) { + var msg = elem.id + " can't restart in " + state + " state"; + ok(restart, msg); + } else { + var msg = elem.id + " can restart in " + state + " state"; + ok(!restart, msg); + } +} + +function main() { + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // At first everything should be starting at 1s + is(always.getStartTime(), 1); + is(whenNotActive.getStartTime(), 1); + is(never.getStartTime(), 1); + + // Now try to restart everything early, should be allowed by all + tryRestart(always, "waiting", true); + tryRestart(whenNotActive, "waiting", true); + tryRestart(never, "waiting", true); + + // Now skip to half-way + var newTime = always.getStartTime() + 0.5 * always.getSimpleDuration(); + svg.setCurrentTime(newTime); + + // Only 'always' should be able to be restarted + tryRestart(always, "active", true); + tryRestart(whenNotActive, "active", false); + tryRestart(never, "active", false); + + // Now skip to the end + newTime = always.getStartTime() + always.getSimpleDuration() + 1; + svg.setCurrentTime(newTime); + + // All animations have finished, so 'always' and 'whenNotActive' should be + // able to be restarted + tryRestart(always, "postactive", true); + tryRestart(whenNotActive, "postactive", true); + tryRestart(never, "postactive", false); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilSetCurrentTime.xhtml b/dom/smil/test/test_smilSetCurrentTime.xhtml new file mode 100644 index 0000000000..91ded84c8c --- /dev/null +++ b/dom/smil/test/test_smilSetCurrentTime.xhtml @@ -0,0 +1,76 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for setCurrentTime Behavior </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" + onload="this.pauseAnimations()" /> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for basic setCurrentTime / getCurrentTime Behavior **/ + +/* Global Variables & Constants */ +const PRECISION_LEVEL = 0.0000001; // Allow small level of floating-point error +const gTimes = [0, 1.5, 0.2, 0.99, -400.5, 10000000, -1]; +const gWaitTime = 20; +var gSvg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); +SimpleTest.requestFlakyTimeout("untriaged"); + +function main() { + ok(gSvg.animationsPaused(), "should be paused by <svg> load handler"); + is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // Test that seeking takes effect immediately + for (var i = 0; i < gTimes.length; i++) { + gSvg.setCurrentTime(gTimes[i]); + // We adopt the SVGT1.2 behavior of clamping negative times to 0 + assertFloatsEqual(gSvg.getCurrentTime(), Math.max(gTimes[i], 0.0)); + } + + // Test that seeking isn't messed up by timeouts + // (using tail recursion to set up the chain of timeout function calls) + var func = function() { + checkTimesAfterIndex(0); + } + setTimeout(func, gWaitTime); +} + +/* This method seeks to the time at gTimes[index], + * and then sets up a timeout to... + * - verify that the seek worked + * - make a recursive call for the next index. + */ +function checkTimesAfterIndex(index) { + if (index == gTimes.length) { + // base case -- we're done! + SimpleTest.finish(); + return; + } + + gSvg.setCurrentTime(gTimes[index]); + var func = function() { + assertFloatsEqual(gSvg.getCurrentTime(), Math.max(gTimes[index], 0.0)); + checkTimesAfterIndex(index + 1); + } + setTimeout(func, gWaitTime); +} + +function assertFloatsEqual(aVal, aExpected) { + ok(Math.abs(aVal - aExpected) <= PRECISION_LEVEL, + "getCurrentTime returned " + aVal + " after seeking to " + aExpected) +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilSync.xhtml b/dom/smil/test/test_smilSync.xhtml new file mode 100644 index 0000000000..36b2a91198 --- /dev/null +++ b/dom/smil/test/test_smilSync.xhtml @@ -0,0 +1,255 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL sync behaviour </title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="20" to="100" + begin="indefinite" dur="4s" restart="always" id="anim1"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="0" to="50" + begin="0" dur="1s" additive="sum" fill="freeze" id="anim2"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="0" to="50" + begin="0" dur="10s" additive="sum" fill="freeze" id="anim3"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="0" to="50" + begin="0" dur="10s" additive="sum" fill="freeze" id="anim4"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="0" to="50" + begin="0" dur="40s" additive="sum" fill="freeze" id="anim5"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animate attributeName="cx" attributeType="XML" from="20" to="100" + begin="100s" dur="4s" restart="always" id="anim6"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL sync behavior **/ + +/* Global Variables */ +var svg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); + +function main() { + testBeginAt(document.getElementById("anim1")); + testChangeBaseVal(document.getElementById("anim2")); + testChangeWhilePaused(document.getElementById("anim3")); + testChangeAnimAttribute(document.getElementById("anim4")); + testChangeTimingAttribute(document.getElementById("anim5")); + testSetCurrentTime(document.getElementById("anim6")); + SimpleTest.finish(); +} + +function testBeginAt(anim) { + // This (hugely important) test checks that a call to beginElement updates to + // the new interval + + // Check some pre-conditions + is(anim.getAttribute("restart"), "always"); + ok(anim.getSimpleDuration() >= 4); + + // First start the animation + svg.setCurrentTime(2); + anim.beginElement(); + + // Then restart it--twice + svg.setCurrentTime(4); + anim.beginElement(); + anim.beginElementAt(-1); + + // The first restart should win if the state machine has been successfully + // updated. If we get '3' back instead we haven't updated properly. + is(anim.getStartTime(), 4); +} + +function testChangeBaseVal(anim) { + // Check that a change to the base value is updated even after animation is + // frozen + + // preconditions -- element should have ended + try { + anim.getStartTime(); + ok(false, "Element has not ended yet."); + } catch (e) { } + + // check frozen value is applied + var target = anim.targetElement; + is(target.cx.animVal.value, 70); + is(target.cx.baseVal.value, 20); + + // change base val and re-check + target.cx.baseVal.value = 30; + is(target.cx.animVal.value, 80); + is(target.cx.baseVal.value, 30); +} + +function testChangeWhilePaused(anim) { + // Check that a change to the base value is updated even when the animation is + // paused + + svg.pauseAnimations(); + svg.setCurrentTime(anim.getSimpleDuration() / 2); + + // check paused value is applied + var target = anim.targetElement; + is(target.cx.animVal.value, 45); + is(target.cx.baseVal.value, 20); + + // change base val and re-check + target.cx.baseVal.value = 30; + is(target.cx.animVal.value, 55); + is(target.cx.baseVal.value, 30); +} + +function testChangeAnimAttribute(anim) { + // Check that a change to an animation attribute causes an update even when + // the animation is frozen and paused + + // Make sure animation is paused and frozen + svg.pauseAnimations(); + svg.setCurrentTime(anim.getStartTime() + anim.getSimpleDuration() + 1); + + // Check frozen value is applied + var target = anim.targetElement; + is(target.cx.animVal.value, 70); + is(target.cx.baseVal.value, 20); + + // Make the animation no longer additive + anim.removeAttribute("additive"); + is(target.cx.animVal.value, 50); + is(target.cx.baseVal.value, 20); +} + +function testChangeTimingAttribute(anim) { + // Check that a change to a timing attribute causes an update even when + // the animation is paused + + svg.pauseAnimations(); + svg.setCurrentTime(anim.getSimpleDuration() / 2); + + // Check part-way value is applied + var target = anim.targetElement; + is(target.cx.animVal.value, 45); + is(target.cx.baseVal.value, 20); + + // Make the animation no longer additive + anim.setAttribute("dur", String(anim.getSimpleDuration() / 2) + "s"); + is(target.cx.animVal.value, 70); + is(target.cx.baseVal.value, 20); + + // Remove fill + anim.removeAttribute("fill"); + is(target.cx.animVal.value, 20); + is(target.cx.baseVal.value, 20); +} + +function testSetCurrentTime(anim) { + // This test checks that a call to setCurrentTime flushes restarts + // + // Actually, this same scenario arises in test_smilRestart.xhtml but we + // isolate this particular situation here for easier diagnosis if this ever + // fails. + // + // At first we have: + // currentTime begin="100s" + // v v + // Doc time: 0---\/\/\/-------99----------100------- + // + svg.setCurrentTime(99); + is(anim.getStartTime(), 100); + + // Then we restart giving us: + // + // beginElement begin="100s" + // v v + // Doc time: 0---\/\/\/-------99----------100------- + // + // So our current interval is + // + // begin="100s" + // v + // +---------------| + // Doc time: 0---\/\/\/-------99-100-101-102-103----- + // + anim.beginElement(); + is(anim.getStartTime(), svg.getCurrentTime()); + + // Then we skip to half-way, i.e. + // + // currentTime + // v + // begin="100s" + // v + // +---------------| + // Doc time: 0---\/\/\/-------99-100-101-102-103----- + // + // At this point we should flush our restarts and early end the first interval + // and start the second interval, giving us + // + // So our timegraph looks like: + // + // currentTime + // v + // +---------------| + // +---| + // Doc time: 0---\/\/\/-------99-100-101-102-103-104- + // + var newTime = anim.getStartTime() + 0.5 * anim.getSimpleDuration(); + svg.setCurrentTime(newTime); + + // Finally we call beginElement again giving us + // + // currentTime + // v + // +---------------| + // +---| + // +---| + // Doc time: 0---\/\/\/-------99-100-101-102-103-104-105- + // + // If, however, setCurrentTime failed to flush restarts out starting point + // we do come to update the timegraph would be: + // + // beginElementAt + // v + // begin="100s" + // v + // +---------------| + // Doc time: 0---\/\/\/-------99-100-101-102-103----- + // + // And as soon as we encountered the begin="100s" spec we'd do a restart + // according to the SMIL algorithms and a restart involves a reset which + // clears the instance times created by DOM calls and so we'd end up with + // just: + // + // currentTime + // v + // +---------------| + // +---| + // Doc time: 0---\/\/\/-------99-100-101-102-103-104- + // + // Which is probably not what the author intended. + // + anim.beginElement(); + is(anim.getStartTime(), svg.getCurrentTime()); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilSyncTransform.xhtml b/dom/smil/test/test_smilSyncTransform.xhtml new file mode 100644 index 0000000000..79c5cbf0b9 --- /dev/null +++ b/dom/smil/test/test_smilSyncTransform.xhtml @@ -0,0 +1,66 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL sync behaviour for transform types</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="20" cy="20" r="15" fill="blue"> + <animateTransform attributeName="transform" type="rotate" + from="90" to="180" begin="0s" dur="2s" fill="freeze" + additive="sum" id="anim1"/> + </circle> + <circle cx="20" cy="20" r="15" fill="blue"> + <animateTransform attributeName="transform" type="scale" + from="1" to="2" begin="2s" dur="2s" id="anim2"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL sync behavior for transform types **/ + +/* Global Variables */ +var svg = document.getElementById("svg"); + +SimpleTest.waitForExplicitFinish(); + +function main() { + testChangeBaseVal(document.getElementById("anim1")); + SimpleTest.finish(); +} + +function testChangeBaseVal(anim) { + // Check that a change to the base value is updated even after animation is + // frozen + + var target = anim.targetElement; + + var baseList = target.transform.baseVal; + var animList = target.transform.animVal; + + // make sure element has ended + svg.setCurrentTime(anim.getSimpleDuration()); + + // check frozen value is applied + is(baseList.numberOfItems, 0); + is(animList.numberOfItems, 1); + + // change base val and re-check + var newTransform = svg.createSVGTransform(); + newTransform.setScale(1,2); + baseList.appendItem(newTransform); + is(baseList.numberOfItems, 1); + is(animList.numberOfItems, 2); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilSyncbaseTarget.xhtml b/dom/smil/test/test_smilSyncbaseTarget.xhtml new file mode 100644 index 0000000000..496cb6751e --- /dev/null +++ b/dom/smil/test/test_smilSyncbaseTarget.xhtml @@ -0,0 +1,180 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for syncbase targetting</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-20" cy="20" r="15" fill="blue" id="circle"> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="a"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" xml:id="b"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="あ"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="a.b"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="a-b"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="a:b"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="-a"/> + <set attributeName="cx" to="0" begin="2s" dur="1s" id="0"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for syncbase targetting behavior **/ + +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = getElement("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + testSpecs(); + testChangeId(); + testRemoveTimebase(); + + SimpleTest.finish(); +} + +function testSpecs() { + var anim = createAnim(); + + // Sanity check--initial state + ok(noStart(anim), "Unexpected initial value for indefinite start time."); + + var specs = [ [ 'a.begin', 2 ], + [ 'b.begin', 'todo' ], // xml:id support, bug 275196 + [ 'あ.begin', 2 ], // unicode id + [ ' a.begin ', 2 ], // whitespace + [ 'a\\.b.begin', 2 ], // escaping + [ 'a\\-b.begin', 2 ], // escaping + [ 'a:b.begin', 2 ], + // Invalid + [ '-a.begin', 'notok' ], // invalid XML ID + [ '\\-a.begin', 'notok' ], // invalid XML ID + [ '0.begin', 'notok' ], // invalid XML ID + [ '\xB7.begin', 'notok' ], // invalid XML ID + [ '\x7B.begin', 'notok' ], // invalid XML ID + [ '.begin', 'notok' ], + [ ' .end ', 'notok' ], + [ 'a.begin-5a', 'notok' ], + // Offsets + [ ' a.begin + 1min', 2 + 60 ], + [ ' a.begin-0.5s', 1.5 ], + ]; + for (var i = 0; i < specs.length; i++) { + var spec = specs[i][0]; + var expected = specs[i][1]; + anim.setAttribute('begin', spec); + try { + if (typeof(expected) == 'number') { + is(anim.getStartTime(), expected, + "Unexpected start time with spec: " + spec); + } else if (expected == 'todo') { + todo_is(anim.getStartTime(), 2,"Unexpected success with spec: " + spec); + } else { + anim.getStartTime(); + ok(false, "Unexpected success with spec: " + spec); + } + } catch(e) { + if (e.name == "InvalidStateError" && + e.code == DOMException.INVALID_STATE_ERR) { + if (typeof(expected) == 'number') + ok(false, "Failed with spec: " + spec); + else if (expected == 'todo') + todo(false, "Yet to implement: " + spec); + else + ok(true); + } else { + ok(false, "Unexpected exception: " + e + "(with spec: " + spec + ")"); + } + } + } + + anim.remove(); +} + +function testChangeId() { + var anim = createAnim(); + + anim.setAttribute('begin', 'a.begin'); + is(anim.getStartTime(), 2, "Unexpected start time."); + + var a = getElement('a'); + a.setAttribute('id', 'a1'); + ok(noStart(anim), "Unexpected return value after changing target ID."); + + a.setAttribute('id', 'a'); + is(anim.getStartTime(), 2, + "Unexpected start time after resetting target ID."); + + anim.remove(); +} + +function testRemoveTimebase() { + var anim = createAnim(); + anim.setAttribute('begin', 'a.begin'); + ok(!noStart(anim), "Unexpected start time before removing timebase."); + + var circle = getElement('circle'); + var a = getElement('a'); + // Sanity check + is(a, circle.firstElementChild, "Unexpected document structure"); + + // Remove timebase + a.remove(); + ok(noStart(anim), "Unexpected start time after removing timebase."); + + // Reinsert timebase + circle.insertBefore(a, circle.firstElementChild); + ok(!noStart(anim), "Unexpected start time after re-inserting timebase."); + + // Remove dependent element + anim.remove(); + ok(noStart(anim), "Unexpected start time after removing dependent."); + + // Create a new dependent + var anim2 = createAnim(); + anim2.setAttribute('begin', 'a.begin'); + is(anim2.getStartTime(), 2, + "Unexpected start time after adding new dependent."); +} + +function createAnim() { + const svgns="http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('from','0'); + anim.setAttribute('to','100'); + anim.setAttribute('begin','indefinite'); + anim.setAttribute('dur','1s'); + return getElement('circle').appendChild(anim); +} + +function noStart(elem) { + var exceptionCaught = false; + + try { + elem.getStartTime(); + } catch(e) { + exceptionCaught = true; + is (e.name, "InvalidStateError", + "Unexpected exception from getStartTime."); + is (e.code, DOMException.INVALID_STATE_ERR, + "Unexpected exception code from getStartTime."); + } + + return exceptionCaught; +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilTextZoom.xhtml b/dom/smil/test/test_smilTextZoom.xhtml new file mode 100644 index 0000000000..5f65bd778e --- /dev/null +++ b/dom/smil/test/test_smilTextZoom.xhtml @@ -0,0 +1,99 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL Animation Behavior with textZoom</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> + <svg xmlns="http://www.w3.org/2000/svg" width="300px" height="200px" + onload="this.pauseAnimations()"> + <text y="100px" x="0px" style="font-size: 5px"> + abc + <animate attributeName="font-size" attributeType="CSS" fill="freeze" + from="20px" to="40px" begin="1s" dur="1s"/> + </text> + <rect y="100px" x="50px" style="stroke-width: 5px"> + <animate attributeName="stroke-width" attributeType="CSS" fill="freeze" + from="20px" to="40px" begin="1s" dur="1s"/> + </rect> + </svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +SimpleTest.waitForExplicitFinish(); + +// Helper function +function verifyStyle(aNode, aPropertyName, aExpectedVal) +{ + var computedVal = SMILUtil.getComputedStyleSimple(aNode, aPropertyName); + + // Bug 1379908: The computed value of stroke-* properties should be + // serialized with px units, but currently Gecko and Servo don't do that + // when animating these values. + if ('stroke-width' == aPropertyName) { + var expectedVal = SMILUtil.stripPx(aExpectedVal); + var unitlessComputedVal = SMILUtil.stripPx(computedVal); + is(unitlessComputedVal, expectedVal, "computed value of " + aPropertyName); + return; + } + is(computedVal, aExpectedVal, "computed value of " + aPropertyName); +} + +function main() +{ + // Start out pause + var svg = SMILUtil.getSVGRoot(); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + // Set text zoom to 2x + var origTextZoom = SpecialPowers.getTextZoom(window); + SpecialPowers.setTextZoom(window, 2); + + try { + // Verify computed style values at various points during animation. + // * Correct behavior is for the computed values of 'font-size' to be + // the same as their corresponding specified values, since text zoom + // should not affect SVG text elements. + // * I also include tests for an identical animation of the "stroke-width" + // property, which should _not_ be affected by textZoom. + var text = document.getElementsByTagName("text")[0]; + var rect = document.getElementsByTagName("rect")[0]; + + verifyStyle(text, "font-size", "5px"); + verifyStyle(rect, "stroke-width", "5px"); + svg.setCurrentTime(1); + verifyStyle(text, "font-size", "20px"); + verifyStyle(rect, "stroke-width", "20px"); + svg.setCurrentTime(1.5); + verifyStyle(text, "font-size", "30px"); + verifyStyle(rect, "stroke-width", "30px"); + svg.setCurrentTime(2); + verifyStyle(text, "font-size", "40px"); + verifyStyle(rect, "stroke-width", "40px"); + svg.setCurrentTime(3); + verifyStyle(text, "font-size", "40px"); + verifyStyle(rect, "stroke-width", "40px"); + } catch (e) { + // If anything goes wrong, make sure we restore textZoom before bubbling + // the exception upwards, so that we don't mess up subsequent tests. + SpecialPowers.setTextZoom(window, origTextZoom); + + throw e; + } + + // We're done! Restore original text-zoom before finishing + SpecialPowers.setTextZoom(window, origTextZoom); + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilTiming.xhtml b/dom/smil/test/test_smilTiming.xhtml new file mode 100644 index 0000000000..0dc8525382 --- /dev/null +++ b/dom/smil/test/test_smilTiming.xhtml @@ -0,0 +1,291 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL timing</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL timing **/ + +/* Global Variables */ +const svgns = "http://www.w3.org/2000/svg"; +var gSvg = document.getElementById("svg"); +var gCircle = document.getElementById('circle'); + +SimpleTest.waitForExplicitFinish(); + +function main() { + ok(gSvg.animationsPaused(), "should be paused by <svg> load handler"); + is(gSvg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var testCases = Array(); + + const secPerMin = 60; + const secPerHour = secPerMin * 60; + + // In the following tests that compare start times, getStartTime will round + // the start time to three decimal places since we expect our implementation + // to be millisecond accurate. + + // Offset syntax + // -- Basic tests, sign and whitespace + testCases.push(StartTimeTest('3s', 3)); + testCases.push(StartTimeTest('0s', 0)); + testCases.push(StartTimeTest('+2s', 2)); + testCases.push(StartTimeTest('-1s\t\r', -1)); + testCases.push(StartTimeTest('- 1s', -1)); + testCases.push(StartTimeTest(' -1s', -1)); + testCases.push(StartTimeTest(' - 1s', -1)); + testCases.push(StartTimeTest(' \t\n\r-1s', -1)); + testCases.push(StartTimeTest('+\n5s', 5)); + testCases.push(StartTimeTest('-\n5s', -5)); + testCases.push(StartTimeTest('\t 5s', 5)); + // -- These tests are from SMILANIM 3.6.7 + testCases.push(StartTimeTest('02:30:03', 2*secPerHour + 30*secPerMin + 3)); + testCases.push(StartTimeTest('50:00:10.25', 50*secPerHour + 10.25)); + testCases.push(StartTimeTest('02:33', 2*secPerMin + 33)); + testCases.push(StartTimeTest('00:10.5', 10.5)); + testCases.push(StartTimeTest('3.2h', 3.2*secPerHour)); + testCases.push(StartTimeTest('45min', 45*secPerMin)); + testCases.push(StartTimeTest('30s', 30)); + testCases.push(StartTimeTest('5ms', 0.005)); + testCases.push(StartTimeTest('12.467', 12.467)); + testCases.push(StartTimeTest('00.5s', 0.5)); + testCases.push(StartTimeTest('00:00.005', 0.005)); + // -- Additional tests + testCases.push(StartTimeTest('61:59:59', 61*secPerHour + 59*secPerMin + 59)); + testCases.push(StartTimeTest('02:59.999999999999999999999', 3*secPerMin)); + testCases.push(StartTimeTest('1234:23:45', + 1234*secPerHour + 23*secPerMin + 45)); + testCases.push(StartTimeTest('61min', 61*secPerMin)); + testCases.push(StartTimeTest('0:30:03', 30*secPerMin + 3)); + // -- Fractional precision + testCases.push(StartTimeTest('25.4567', 25.457)); + testCases.push(StartTimeTest('0.123456789', 0.123)); + testCases.push(StartTimeTest('0.00000000000000000000001', 0)); + testCases.push(StartTimeTest('-0.00000000000000000000001', 0)); + testCases.push(StartTimeTest('0.0009', 0.001)); + testCases.push(StartTimeTest('0.99999999999999999999999999999999999999', 1)); + testCases.push(StartTimeTest('23.4567ms', 0.023)); + testCases.push(StartTimeTest('23.7ms', 0.024)); + // -- Test errors + testCases.push(StartTimeTest(' + +3s', 'none')); + testCases.push(StartTimeTest(' +-3s', 'none')); + testCases.push(StartTimeTest('1:12:12:12', 'none')); + testCases.push(StartTimeTest('4:50:60', 'none')); + testCases.push(StartTimeTest('4:60:0', 'none')); + testCases.push(StartTimeTest('4:60', 'none')); + testCases.push(StartTimeTest('4:-1:00', 'none')); + testCases.push(StartTimeTest('4 5m', 'none')); + testCases.push(StartTimeTest('4 5ms', 'none')); + testCases.push(StartTimeTest('02:3:03', 'none')); + testCases.push(StartTimeTest('45.7 s', 'none')); + testCases.push(StartTimeTest(' 3 h ', 'none')); + testCases.push(StartTimeTest('2:33 ', 'none')); + testCases.push(StartTimeTest('02:33 2', 'none')); + testCases.push(StartTimeTest('\u000B 02:33', 'none')); + testCases.push(StartTimeTest('h', 'none')); + testCases.push(StartTimeTest('23.s', 'none')); + testCases.push(StartTimeTest('23.', 'none')); + testCases.push(StartTimeTest('23.54.2s', 'none')); + testCases.push(StartTimeTest('23sec', 'none')); + testCases.push(StartTimeTest('five', 'none')); + testCases.push(StartTimeTest('', 'none')); + testCases.push(StartTimeTest('02:33s', 'none')); + testCases.push(StartTimeTest('02:33 s', 'none')); + testCases.push(StartTimeTest('2.54e6', 'none')); + testCases.push(StartTimeTest('02.5:33', 'none')); + testCases.push(StartTimeTest('2:-45:33', 'none')); + testCases.push(StartTimeTest('2:4.5:33', 'none')); + testCases.push(StartTimeTest('45m', 'none')); + testCases.push(StartTimeTest(':20:30', 'none')); + testCases.push(StartTimeTest('1.5:30', 'none')); + testCases.push(StartTimeTest('15:-30', 'none')); + testCases.push(StartTimeTest('::30', 'none')); + testCases.push(StartTimeTest('15:30s', 'none')); + testCases.push(StartTimeTest('2:1.:30', 'none')); + testCases.push(StartTimeTest('2:.1:30', 'none')); + testCases.push(StartTimeTest('2.0:15:30', 'none')); + testCases.push(StartTimeTest('2.:15:30', 'none')); + testCases.push(StartTimeTest('.2:15:30', 'none')); + testCases.push(StartTimeTest('70:15', 'none')); + testCases.push(StartTimeTest('media', 'none')); + testCases.push(StartTimeTest('5mi', 'none')); + testCases.push(StartTimeTest('5hours', 'none')); + testCases.push(StartTimeTest('h05:30', 'none')); + testCases.push(StartTimeTest('05:40\x9A', 'none')); + testCases.push(StartTimeTest('05:40\u30D5', 'none')); + testCases.push(StartTimeTest('05:40β', 'none')); + + // List syntax + testCases.push(StartTimeTest('3', 3)); + testCases.push(StartTimeTest('3;', 3)); + testCases.push(StartTimeTest('3; ', 3)); + testCases.push(StartTimeTest('3 ; ', 3)); + testCases.push(StartTimeTest('3;;', 3)); + testCases.push(StartTimeTest('3;; ', 3)); + testCases.push(StartTimeTest(';3', 3)); + testCases.push(StartTimeTest(' ;3', 3)); + testCases.push(StartTimeTest('3;4', 3)); + testCases.push(StartTimeTest(' 3 ; 4 ', 3)); + + // List syntax on end times + testCases.push({ + 'attr' : { 'begin': '0s', + 'end': '1s; 2s' }, + 'times': [ [ 0, 0 ], + [ 1, -100 ] ] + }); + testCases.push({ + 'attr' : { 'begin': '0s', + 'end': '1s; 2s; ' }, + 'times': [ [ 0, 0 ], + [ 1, -100 ] ] + }); + testCases.push({ + 'attr' : { 'begin': '0s', + 'end': '3s; 2s' }, + 'times': [ [ 0, 0 ], + [ 1, 10 ], + [ 2, -100 ] ] + }); + + // Simple case + testCases.push({ + 'attr' : { 'begin': '3s' }, + 'times': [ [ 0, -100 ], + [ 4, 10 ] ] + }); + + // Multiple begins + testCases.push({ + 'attr' : { 'begin': '2s; 6s', + 'dur': '2s' }, + 'times': [ [ 0, -100 ], + [ 3, 50 ], + [ 4, -100 ], + [ 7, 50 ], + [ 8, -100 ] ] + }); + + // Negative begins + testCases.push({ + 'attr' : { 'begin': '-3s; 1s ; 4s', + 'dur': '2s ', + 'fill': 'freeze' }, + 'times': [ [ 0, -100 ], + [ 0.5, -100 ], + [ 1, 0 ], + [ 2, 50 ], + [ 3, 100 ], + [ 5, 50 ] ] + }); + + // Sorting + testCases.push({ + 'attr' : { 'begin': '-3s; 110s; 1s; 4s; -5s; -10s', + 'end': '111s; -5s; -15s; 6s; -5s; 1.2s', + 'dur': '2s ', + 'fill': 'freeze' }, + 'times': [ [ 0, -100 ], + [ 1, 0 ], + [ 2, 10 ], + [ 4, 0 ], + [ 5, 50 ], + [ 109, 100 ], + [ 110, 0 ], + [ 112, 50 ] ] + }); + + for (var i = 0; i < testCases.length; i++) { + gSvg.setCurrentTime(0); + var test = testCases[i]; + + // Generate string version of params for output messages + var params = ""; + for (var name in test.attr) { + params += name + '="' + test.attr[name] + '" '; + } + params = params.trim(); + + // Create animation elements + var anim = createAnim(test.attr); + + // Run samples + if ('times' in test) { + for (var j = 0; j < test.times.length; j++) { + var curSample = test.times[j]; + checkSample(curSample[0], curSample[1], params); + } + } + + // Check start time + if ('startTime' in test) { + is(getStartTime(anim), test.startTime, + "Got unexpected start time for " + params); + } + + anim.remove(); + } + + SimpleTest.finish(); +} + +function createAnim(attr) { + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('from','0'); + anim.setAttribute('to','100'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','indefinite'); + for (name in attr) { + anim.setAttribute(name, attr[name]); + } + return gCircle.appendChild(anim); +} + +function checkSample(time, expectedValue, params) { + gSvg.setCurrentTime(time); + var msg = "Unexpected sample value for " + params + + " at t=" + time + ": "; + is(gCircle.cx.animVal.value, expectedValue); +} + +function getStartTime(anim) { + var startTime; + try { + startTime = anim.getStartTime(); + // We round start times to 3 decimal places to make comparisons simpler + startTime = parseFloat(startTime.toFixed(3)); + } catch(e) { + if (e.name == "InvalidStateError" && + e.code == DOMException.INVALID_STATE_ERR) { + startTime = 'none'; + } else { + ok(false, "Unexpected exception: " + e); + } + } + return startTime; +} + +function StartTimeTest(beginSpec, expectedStartTime) { + return { 'attr' : { 'begin': beginSpec }, + 'startTime': expectedStartTime }; +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilTimingZeroIntervals.xhtml b/dom/smil/test/test_smilTimingZeroIntervals.xhtml new file mode 100644 index 0000000000..d54b74600b --- /dev/null +++ b/dom/smil/test/test_smilTimingZeroIntervals.xhtml @@ -0,0 +1,285 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL timing with zero-duration intervals</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL timing with zero-duration intervals **/ + +/* Global Variables */ +const svgns="http://www.w3.org/2000/svg"; +var svg = document.getElementById("svg"); +var circle = document.getElementById('circle'); + +SimpleTest.waitForExplicitFinish(); + +function createAnim() { + var anim = document.createElementNS(svgns,'animate'); + anim.setAttribute('attributeName','cx'); + anim.setAttribute('from','0'); + anim.setAttribute('to','100'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','indefinite'); + return circle.appendChild(anim); +} + +function removeAnim(anim) { + anim.remove(); +} + +function main() { + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var tests = + [ testZeroDurationIntervalsA, + testZeroDurationIntervalsB, + testZeroDurationIntervalsC, + testZeroDurationIntervalsD, + testZeroDurationIntervalsE, + testZeroDurationIntervalsF, + testZeroDurationIntervalsG, + testZeroDurationIntervalsH, + testZeroDurationIntervalsI, + testZeroDurationIntervalsJ, + testZeroDurationIntervalsK, + testZeroDurationIntervalsL, + testZeroDurationIntervalsM, + testZeroDurationIntervalsN, + testZeroDurationIntervalsO + ]; + for (var i = 0; i < tests.length; i++) { + var anim = createAnim(); + svg.setCurrentTime(0); + tests[i](anim); + removeAnim(anim); + } + SimpleTest.finish(); +} + +function checkSample(time, expectedValue) { + svg.setCurrentTime(time); + is(circle.cx.animVal.value, expectedValue); +} + +function testZeroDurationIntervalsA(anim) { + // The zero-duration interval should play, followed by a second interval + // starting at the same point. There is no end for the interval + // at 4s so it should not play. + anim.setAttribute('begin', '1s ;4s'); + anim.setAttribute('end', '1s; 2s'); + anim.setAttribute('dur', '2s '); + anim.setAttribute('fill', 'freeze'); + checkSample(0,-100); + checkSample(1,0); + checkSample(1.1,5); + checkSample(2,50); + checkSample(3,50); + checkSample(4,50); + checkSample(5,50); + checkSample(6,50); +} + +function testZeroDurationIntervalsB(anim) { + // This interval should however actually restart as there is a valid end-point + anim.setAttribute('begin', '1s ;4s'); + anim.setAttribute('end', '1.1s; indefinite'); + anim.setAttribute('dur', '2s '); + anim.setAttribute('fill', 'freeze'); + checkSample(0,-100); + checkSample(1,0); + checkSample(1.1,5); + checkSample(2,5); + checkSample(4,0); + checkSample(5,50); +} + +function testZeroDurationIntervalsC(anim) { + // -0.5s has already been used as the endpoint of one interval so don't use it + // a second time + anim.setAttribute('begin', '-2s; -0.5s'); + anim.setAttribute('end', '-0.5s; 1s'); + anim.setAttribute('dur', '2s'); + anim.setAttribute('fill', 'freeze'); + checkSample(0,25); + checkSample(1.5,75); +} + +function testZeroDurationIntervalsD(anim) { + // Two end points that could make a zero-length interval + anim.setAttribute('begin', '-2s; -0.5s'); + anim.setAttribute('end', '-0.5s; -0.5s; 1s'); + anim.setAttribute('dur', '2s'); + anim.setAttribute('fill', 'freeze'); + checkSample(0,25); + checkSample(1.5,75); +} + +function testZeroDurationIntervalsE(anim) { + // Should give us 1s-1s, 1s-5s + anim.setAttribute('begin', '1s'); + anim.setAttribute('end', '1s; 5s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(6,40); +} + +function testZeroDurationIntervalsF(anim) { + // Should give us 1s-1s + anim.setAttribute('begin', '1s'); + anim.setAttribute('end', '1s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,0); + try { + anim.getStartTime(); + ok(false, "Failed to throw exception when there's no current interval."); + } catch (e) { } +} + +function testZeroDurationIntervalsG(anim) { + // Test a non-zero interval after a zero interval + // Should give us 1-2s, 3-3s, 3-4s + anim.setAttribute('begin', '1s; 3s'); + anim.setAttribute('end', '3s; 5s'); + anim.setAttribute('dur', '1s'); + anim.setAttribute('fill', 'freeze'); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,100); + checkSample(3,0); + checkSample(5,100); +} + +function testZeroDurationIntervalsH(anim) { + // Test multiple non-adjacent zero-intervals + // Should give us 1-1s, 1-2s, 3-3s, 3-4s + anim.setAttribute('begin', '1s; 3s'); + anim.setAttribute('end', '1s; 3s; 5s'); + anim.setAttribute('dur', '1s'); + anim.setAttribute('fill', 'freeze'); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,100); + checkSample(3,0); + checkSample(5,100); +} + +function testZeroDurationIntervalsI(anim) { + // Test skipping values that are the same + // Should give us 1-1s, 1-2s + anim.setAttribute('begin', '1s; 1s'); + anim.setAttribute('end', '1s; 1s; 2s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,10); + checkSample(3,10); +} + +function testZeroDurationIntervalsJ(anim) { + // Should give us 0-0.5s, 1-1s, 1-3s + anim.setAttribute('begin', '0s; 1s; 1s'); + anim.setAttribute('end', '1s; 3s'); + anim.setAttribute('dur', '0.5s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),0); + checkSample(0,0); + checkSample(0.6,100); + checkSample(1,0); + checkSample(2,100); +} + +function testZeroDurationIntervalsK(anim) { + // Should give us -0.5-1s + anim.setAttribute('begin', '-0.5s'); + anim.setAttribute('end', '-0.5s; 1s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),-0.5); + checkSample(0,5); + checkSample(1,15); + checkSample(2,15); +} + +function testZeroDurationIntervalsL(anim) { + // Test that multiple end values are ignored + // Should give us 1-1s, 1-3s + anim.setAttribute('begin', '1s'); + anim.setAttribute('end', '1s; 1s; 1s; 3s'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(2,10); + checkSample(4,20); +} + +function testZeroDurationIntervalsM(anim) { + // Test 0-duration interval at start + anim.setAttribute('begin', '0s'); + anim.setAttribute('end', '0s'); + anim.setAttribute('fill', 'freeze'); + try { + anim.getStartTime(); + ok(false, "Failed to throw exception when there's no current interval."); + } catch (e) { } + checkSample(0,0); + checkSample(1,0); +} + +function testZeroDurationIntervalsN(anim) { + // Test 0-active-duration interval at start (different code path to above) + anim.setAttribute('begin', '0s'); + anim.setAttribute('repeatDur', '0s'); + anim.setAttribute('fill', 'freeze'); + try { + anim.getStartTime(); + ok(false, "Failed to throw exception when there's no current interval."); + } catch (e) { } + checkSample(0,0); + checkSample(1,0); +} + +function testZeroDurationIntervalsO(anim) { + // Make a zero-duration interval by constraining the active duration + // We should not loop infinitely but should look for the next begin time after + // that (in this case that is 2s, which would otherwise have been skipped + // because restart=whenNotActive) + // Should give us 1-1s, 2-2s + anim.setAttribute('begin', '1s; 2s'); + anim.setAttribute('repeatDur', '0s'); + anim.setAttribute('restart', 'whenNotActive'); + anim.setAttribute('fill', 'freeze'); + is(anim.getStartTime(),1); + checkSample(0,-100); + checkSample(1,0); + checkSample(1.5,0); + checkSample(3,0); + try { + anim.getStartTime(); + ok(false, "Failed to throw exception when there's no current interval."); + } catch (e) { } +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilUpdatedInterval.xhtml b/dom/smil/test/test_smilUpdatedInterval.xhtml new file mode 100644 index 0000000000..3045a815de --- /dev/null +++ b/dom/smil/test/test_smilUpdatedInterval.xhtml @@ -0,0 +1,64 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Tests updated intervals</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<p id="display"></p> +<div id="content" style="display: none"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px" + onload="this.pauseAnimations()"> + <circle cx="20" cy="20" r="15" fill="blue" id="circle"> + <animate attributeName="cx" from="0" to="100" begin="2s" dur="4s" + id="anim1" attributeType="XML"/> + </circle> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Tests for updated intervals **/ + +/* Global Variables */ +SimpleTest.waitForExplicitFinish(); + +function main() { + var svg = document.getElementById("svg"); + ok(svg.animationsPaused(), "should be paused by <svg> load handler"); + is(svg.getCurrentTime(), 0, "should be paused at 0 in <svg> load handler"); + + var anim = document.getElementById("anim1"); + + // Check regular operation + svg.setCurrentTime(3); + is(anim.getStartTime(), 2, "Unexpected initial start time"); + + // Add an instance time before the current interval at t=1s + anim.beginElementAt(-2); + + // We shouldn't change the begin time + is(anim.getStartTime(), 2, "Start time shouldn't have changed"); + + // Or the end--that is, if we go to t=5.5 we should still be running + svg.setCurrentTime(5.5); + try { + is(anim.getSimpleDuration(), 4, "Simple duration shouldn't have changed"); + is(anim.getStartTime(), 2, "Start time shouldn't have changed after seek"); + } catch (e) { + if (e.name != "InvalidStateError" || + e.code != DOMException.INVALID_STATE_ERR) + throw e; + ok(false, "Animation ended too early, even though begin time and " + + "simple duration didn't change"); + } + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilValues.xhtml b/dom/smil/test/test_smilValues.xhtml new file mode 100644 index 0000000000..b25a153472 --- /dev/null +++ b/dom/smil/test/test_smilValues.xhtml @@ -0,0 +1,171 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL values</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=557885">Mozilla Bug + 474742</a> +<p id="display"></p> +<div id="content"> +<svg id="svg" xmlns="http://www.w3.org/2000/svg" width="120px" height="120px"> + <circle cx="-100" cy="20" r="15" fill="blue" id="circle"/> +</svg> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL values **/ + +var gSvg = document.getElementById("svg"); +SimpleTest.waitForExplicitFinish(); + +function main() +{ + gSvg.pauseAnimations(); + + var testCases = Array(); + + // Single value + testCases.push({ + 'attr' : { 'values': 'a' }, + 'times': [ [ 0, 'a' ] ] + }); + + // The parsing below is based on the following discussion: + // + // http://lists.w3.org/Archives/Public/www-svg/2011Nov/0136.html + // + // In summary: + // * Values lists are semi-colon delimited and semi-colon terminated. + // * However, if there are extra non-whitespace characters after the final + // semi-colon then there's an implied semi-colon at the end. + // + // This differs to what is specified in SVG 1.1 but is consistent with the + // majority of browsers and with existing content (particularly that generated + // by Ikivo Animator). + + // Trailing semi-colon + testCases.push({ + 'attr' : { 'values': 'a;' }, + 'times': [ [ 0, 'a' ], [ 10, 'a' ] ] + }); + + // Trailing semi-colon + whitespace + testCases.push({ + 'attr' : { 'values': 'a; ' }, + 'times': [ [ 0, 'a' ], [ 10, 'a' ] ] + }); + + // Whitespace + trailing semi-colon + testCases.push({ + 'attr' : { 'values': 'a ;' }, + 'times': [ [ 0, 'a' ], [ 10, 'a' ] ] + }); + + // Empty at end + testCases.push({ + 'attr' : { 'values': 'a;;' }, + 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, '' ] ] + }); + + // Empty at end + whitespace + testCases.push({ + 'attr' : { 'values': 'a;; ' }, + 'times': [ [ 0, 'a' ], [ 4, 'a' ], [ 5, '' ], [ 10, '' ] ] + }); + + // Empty in middle + testCases.push({ + 'attr' : { 'values': 'a;;b' }, + 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, 'b' ] ] + }); + + // Empty in middle + trailing semi-colon + testCases.push({ + 'attr' : { 'values': 'a;;b;' }, + 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, 'b' ] ] + }); + + // Whitespace in middle + testCases.push({ + 'attr' : { 'values': 'a; ;b' }, + 'times': [ [ 0, 'a' ], [ 5, '' ], [ 10, 'b' ] ] + }); + + // Empty at start + testCases.push({ + 'attr' : { 'values': ';a' }, + 'times': [ [ 0, '' ], [ 5, 'a' ], [ 10, 'a' ] ] + }); + + // Whitespace at start + testCases.push({ + 'attr' : { 'values': ' ;a' }, + 'times': [ [ 0, '' ], [ 5, 'a' ], [ 10, 'a' ] ] + }); + + // Embedded whitespace + testCases.push({ + 'attr' : { 'values': ' a b ; c d ' }, + 'times': [ [ 0, 'a b' ], [ 5, 'c d' ], [ 10, 'c d' ] ] + }); + + // Whitespace only + testCases.push({ + 'attr' : { 'values': ' ' }, + 'times': [ [ 0, '' ], [ 10, '' ] ] + }); + + for (var i = 0; i < testCases.length; i++) { + gSvg.setCurrentTime(0); + var test = testCases[i]; + + // Create animation elements + var anim = createAnim(test.attr); + + // Run samples + for (var j = 0; j < test.times.length; j++) { + var curSample = test.times[j]; + gSvg.setCurrentTime(curSample[0]); + checkSample(anim, curSample[1], curSample[0], i); + } + + anim.remove(); + } + + SimpleTest.finish(); +} + +function createAnim(attr) +{ + const svgns = "http://www.w3.org/2000/svg"; + var anim = document.createElementNS(svgns, 'animate'); + anim.setAttribute('attributeName','class'); + anim.setAttribute('dur','10s'); + anim.setAttribute('begin','0s'); + anim.setAttribute('fill','freeze'); + for (name in attr) { + anim.setAttribute(name, attr[name]); + } + return document.getElementById('circle').appendChild(anim); +} + +function checkSample(anim, expectedValue, sampleTime, caseNum) +{ + var msg = "Test case " + caseNum + + " (values: '" + anim.getAttribute('values') + "')," + + "t=" + sampleTime + + ": Unexpected sample value:"; + is(typeof anim.targetElement.className, "object"); + is(anim.targetElement.className.animVal, expectedValue, msg); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> diff --git a/dom/smil/test/test_smilWithTransition.html b/dom/smil/test/test_smilWithTransition.html new file mode 100644 index 0000000000..f464f9a39d --- /dev/null +++ b/dom/smil/test/test_smilWithTransition.html @@ -0,0 +1,18 @@ +<!doctype html> +<meta charset=utf-8> +<head> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<pre id="test"> +<script> +'use strict'; + +SimpleTest.waitForExplicitFinish(); +SpecialPowers.pushPrefEnv( + { "set": [["dom.animations-api.getAnimations.enabled", true]]}, + function() { + window.open("file_smilWithTransition.html"); + }); +</script> +</html> diff --git a/dom/smil/test/test_smilWithXlink.xhtml b/dom/smil/test/test_smilWithXlink.xhtml new file mode 100644 index 0000000000..16c6016deb --- /dev/null +++ b/dom/smil/test/test_smilWithXlink.xhtml @@ -0,0 +1,48 @@ +<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>Test for animate with xlink:href attribute.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <style>
+ div#target {
+ width: 300px;
+ height: 100px;
+ background-color: red;
+ }
+ </style>
+</head>
+<body>
+<p id="display"></p>
+<div id="target"></div>
+<svg xmlns="http://www.w3.org/2000/svg"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ id="svg">
+ <animate xlink:href="#target"
+ attributeName="width" from="0" to="200" dur="10s" fill="freeze"/>
+</svg>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+<![CDATA[
+SimpleTest.waitForExplicitFinish();
+
+function runTest() {
+ var svg = document.getElementById("svg");
+ var target = document.getElementById("target");
+
+ svg.pauseAnimations();
+ svg.setCurrentTime(5);
+
+ var cs = getComputedStyle(target);
+ is(cs.width, "100px", "SMIL should affect outer element.");
+
+ SimpleTest.finish();
+ return;
+}
+
+window.addEventListener("load", runTest);
+
+]]>
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/smil/test/test_smilXHR.xhtml b/dom/smil/test/test_smilXHR.xhtml new file mode 100644 index 0000000000..eb0f84268c --- /dev/null +++ b/dom/smil/test/test_smilXHR.xhtml @@ -0,0 +1,88 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Test for SMIL Behavior in Data Documents</title> + <script src="/tests/SimpleTest/SimpleTest.js"></script> + <script type="text/javascript" src="smilTestUtils.js"></script> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> +</head> +<body> +<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529387">Mozilla Bug 529387</a> +<p id="display"></p> +<div id="content" style="display: none"> +</div> +<pre id="test"> +<script class="testbody" type="text/javascript"> +<![CDATA[ +/** Test for SMIL Behavior in Data Documents, with XMLHttpRequest **/ + +SimpleTest.waitForExplicitFinish(); + +function tryPausing(svg) { + // Check that pausing has no effect + ok(!svg.animationsPaused(), + "shouldn't be paused (because we shouldn't have even started"); + svg.pauseAnimations(); + ok(!svg.animationsPaused(), "attempts to pause should have no effect"); + svg.unpauseAnimations(); + ok(!svg.animationsPaused(), "still shouldn't be paused, after pause/unpause"); +} + +function trySeeking(svg) { + // Check that seeking is ineffective + is(svg.getCurrentTime(), 0, "should start out at time=0"); + svg.setCurrentTime(1); + is(svg.getCurrentTime(), 0, "shouldn't be able to seek away from time=0"); +} + +function tryBeginEnd(anim) { + // Check that beginning / ending a particular animation element will trigger + // exceptions. + var didThrow = false; + ok(anim, "need a non-null animate element"); + try { + anim.beginElement(); + } catch (e) { + didThrow = true; + } + ok(didThrow, "beginElement should fail"); + + didThrow = false; + try { + anim.endElement(); + } catch (e) { + didThrow = true; + } + ok(didThrow, "endElement should fail"); +} + +function main() { + var xhr = new XMLHttpRequest(); + xhr.open("GET", "smilXHR_helper.svg", false); + xhr.send(); + var xdoc = xhr.responseXML; + + var svg = xdoc.getElementById("svg"); + var circ = xdoc.getElementById("circ"); + var animXML = xdoc.getElementById("animXML"); + var animCSS = xdoc.getElementById("animCSS"); + + tryPausing(svg); + trySeeking(svg); + tryBeginEnd(animXML); + tryBeginEnd(animCSS); + + // Check that the actual values of our animated attr/prop aren't affected + is(circ.cx.animVal.value, circ.cx.baseVal.value, + "animation of attribute shouldn't be taking effect"); + is(SMILUtil.getComputedStyleSimple(circ, "opacity"), "1", + "animation of CSS property shouldn't be taking effect"); + + SimpleTest.finish(); +} + +window.addEventListener("load", main); +]]> +</script> +</pre> +</body> +</html> |