blob: b4e354f0b609f60fef973184c5c2dedfc0544eac [file] [log] [blame]
/*
* Copyright (C) 2008 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include "core/svg/animation/SMILTimeContainer.h"
#include "core/animation/AnimationClock.h"
#include "core/animation/DocumentTimeline.h"
#include "core/dom/ElementTraversal.h"
#include "core/frame/FrameView.h"
#include "core/frame/Settings.h"
#include "core/frame/UseCounter.h"
#include "core/svg/SVGSVGElement.h"
#include "core/svg/animation/SMILTime.h"
#include "core/svg/animation/SVGSMILElement.h"
#include <algorithm>
namespace blink {
static const double initialFrameDelay = 0.025;
static const double animationPolicyOnceDuration = 3.000;
SMILTimeContainer::SMILTimeContainer(SVGSVGElement& owner)
: m_presentationTime(0),
m_referenceTime(0),
m_frameSchedulingState(Idle),
m_started(false),
m_paused(false),
m_documentOrderIndexesDirty(false),
m_wakeupTimer(this, &SMILTimeContainer::wakeupTimerFired),
m_animationPolicyOnceTimer(this,
&SMILTimeContainer::animationPolicyTimerFired),
m_ownerSVGElement(&owner)
#if ENABLE(ASSERT)
,
m_preventScheduledAnimationsChanges(false)
#endif
{
}
SMILTimeContainer::~SMILTimeContainer() {
cancelAnimationFrame();
cancelAnimationPolicyTimer();
ASSERT(!m_wakeupTimer.isActive());
#if ENABLE(ASSERT)
ASSERT(!m_preventScheduledAnimationsChanges);
#endif
}
void SMILTimeContainer::schedule(SVGSMILElement* animation,
SVGElement* target,
const QualifiedName& attributeName) {
ASSERT(animation->timeContainer() == this);
ASSERT(target);
ASSERT(animation->hasValidAttributeName());
ASSERT(animation->hasValidAttributeType());
ASSERT(animation->inActiveDocument());
#if ENABLE(ASSERT)
ASSERT(!m_preventScheduledAnimationsChanges);
#endif
ElementAttributePair key(target, attributeName);
Member<AnimationsLinkedHashSet>& scheduled =
m_scheduledAnimations.add(key, nullptr).storedValue->value;
if (!scheduled)
scheduled = new AnimationsLinkedHashSet;
ASSERT(!scheduled->contains(animation));
scheduled->add(animation);
SMILTime nextFireTime = animation->nextProgressTime();
if (nextFireTime.isFinite())
notifyIntervalsChanged();
}
void SMILTimeContainer::unschedule(SVGSMILElement* animation,
SVGElement* target,
const QualifiedName& attributeName) {
ASSERT(animation->timeContainer() == this);
#if ENABLE(ASSERT)
ASSERT(!m_preventScheduledAnimationsChanges);
#endif
ElementAttributePair key(target, attributeName);
GroupedAnimationsMap::iterator it = m_scheduledAnimations.find(key);
ASSERT(it != m_scheduledAnimations.end());
AnimationsLinkedHashSet* scheduled = it->value.get();
ASSERT(scheduled);
AnimationsLinkedHashSet::iterator itAnimation = scheduled->find(animation);
ASSERT(itAnimation != scheduled->end());
scheduled->remove(itAnimation);
if (scheduled->isEmpty())
m_scheduledAnimations.remove(it);
}
bool SMILTimeContainer::hasAnimations() const {
return !m_scheduledAnimations.isEmpty();
}
bool SMILTimeContainer::hasPendingSynchronization() const {
return m_frameSchedulingState == SynchronizeAnimations &&
m_wakeupTimer.isActive() && !m_wakeupTimer.nextFireInterval();
}
void SMILTimeContainer::notifyIntervalsChanged() {
if (!isStarted())
return;
// Schedule updateAnimations() to be called asynchronously so multiple
// intervals can change with updateAnimations() only called once at the end.
if (hasPendingSynchronization())
return;
cancelAnimationFrame();
scheduleWakeUp(0, SynchronizeAnimations);
}
double SMILTimeContainer::elapsed() const {
if (!isStarted())
return 0;
if (isPaused())
return m_presentationTime;
return m_presentationTime +
(document().timeline().currentTimeInternal() - m_referenceTime);
}
void SMILTimeContainer::synchronizeToDocumentTimeline() {
m_referenceTime = document().timeline().currentTimeInternal();
}
bool SMILTimeContainer::isPaused() const {
// If animation policy is "none", the timeline is always paused.
return m_paused || animationPolicy() == ImageAnimationPolicyNoAnimation;
}
bool SMILTimeContainer::isStarted() const {
return m_started;
}
bool SMILTimeContainer::isTimelineRunning() const {
return isStarted() && !isPaused();
}
void SMILTimeContainer::start() {
RELEASE_ASSERT(!isStarted());
if (!document().isActive())
return;
if (!handleAnimationPolicy(RestartOnceTimerIfNotPaused))
return;
// Sample the document timeline to get a time reference for the "presentation
// time".
synchronizeToDocumentTimeline();
m_started = true;
// If the "presentation time" is non-zero, the timeline was modified via
// setElapsed() before the document began. In this case pass on
// 'seekToTime=true' to updateAnimations() to issue a seek.
SMILTime earliestFireTime =
updateAnimations(m_presentationTime, m_presentationTime ? true : false);
if (!canScheduleFrame(earliestFireTime))
return;
// If the timeline is running, and there are pending animation updates,
// always perform the first update after the timeline was started using
// the wake-up mechanism.
double delayTime = earliestFireTime.value() - m_presentationTime;
scheduleWakeUp(std::max(initialFrameDelay, delayTime), SynchronizeAnimations);
}
void SMILTimeContainer::pause() {
if (!handleAnimationPolicy(CancelOnceTimer))
return;
DCHECK(!isPaused());
if (isStarted()) {
m_presentationTime = elapsed();
cancelAnimationFrame();
}
// Update the flag after sampling elapsed().
m_paused = true;
}
void SMILTimeContainer::resume() {
if (!handleAnimationPolicy(RestartOnceTimer))
return;
DCHECK(isPaused());
m_paused = false;
if (!isStarted())
return;
synchronizeToDocumentTimeline();
scheduleWakeUp(0, SynchronizeAnimations);
}
void SMILTimeContainer::setElapsed(double elapsed) {
m_presentationTime = elapsed;
// If the document hasn't finished loading, |m_presentationTime| will be
// used as the start time to seek to once it's possible.
if (!isStarted())
return;
if (!handleAnimationPolicy(RestartOnceTimerIfNotPaused))
return;
cancelAnimationFrame();
if (!isPaused())
synchronizeToDocumentTimeline();
#if ENABLE(ASSERT)
m_preventScheduledAnimationsChanges = true;
#endif
for (const auto& entry : m_scheduledAnimations) {
if (!entry.key.first)
continue;
AnimationsLinkedHashSet* scheduled = entry.value.get();
for (SVGSMILElement* element : *scheduled)
element->reset();
}
#if ENABLE(ASSERT)
m_preventScheduledAnimationsChanges = false;
#endif
updateAnimationsAndScheduleFrameIfNeeded(elapsed, true);
}
void SMILTimeContainer::scheduleAnimationFrame(double delayTime) {
DCHECK(std::isfinite(delayTime));
DCHECK(isTimelineRunning());
DCHECK(!m_wakeupTimer.isActive());
if (delayTime < AnimationTimeline::s_minimumDelay) {
serviceOnNextFrame();
} else {
scheduleWakeUp(delayTime - AnimationTimeline::s_minimumDelay,
FutureAnimationFrame);
}
}
void SMILTimeContainer::cancelAnimationFrame() {
m_frameSchedulingState = Idle;
m_wakeupTimer.stop();
}
void SMILTimeContainer::scheduleWakeUp(
double delayTime,
FrameSchedulingState frameSchedulingState) {
ASSERT(frameSchedulingState == SynchronizeAnimations ||
frameSchedulingState == FutureAnimationFrame);
m_wakeupTimer.startOneShot(delayTime, BLINK_FROM_HERE);
m_frameSchedulingState = frameSchedulingState;
}
void SMILTimeContainer::wakeupTimerFired(TimerBase*) {
ASSERT(m_frameSchedulingState == SynchronizeAnimations ||
m_frameSchedulingState == FutureAnimationFrame);
if (m_frameSchedulingState == FutureAnimationFrame) {
ASSERT(isTimelineRunning());
m_frameSchedulingState = Idle;
serviceOnNextFrame();
} else {
m_frameSchedulingState = Idle;
updateAnimationsAndScheduleFrameIfNeeded(elapsed());
}
}
void SMILTimeContainer::scheduleAnimationPolicyTimer() {
m_animationPolicyOnceTimer.startOneShot(animationPolicyOnceDuration,
BLINK_FROM_HERE);
}
void SMILTimeContainer::cancelAnimationPolicyTimer() {
if (m_animationPolicyOnceTimer.isActive())
m_animationPolicyOnceTimer.stop();
}
void SMILTimeContainer::animationPolicyTimerFired(TimerBase*) {
pause();
}
ImageAnimationPolicy SMILTimeContainer::animationPolicy() const {
Settings* settings = document().settings();
if (!settings)
return ImageAnimationPolicyAllowed;
return settings->imageAnimationPolicy();
}
bool SMILTimeContainer::handleAnimationPolicy(
AnimationPolicyOnceAction onceAction) {
ImageAnimationPolicy policy = animationPolicy();
// If the animation policy is "none", control is not allowed.
// returns false to exit flow.
if (policy == ImageAnimationPolicyNoAnimation)
return false;
// If the animation policy is "once",
if (policy == ImageAnimationPolicyAnimateOnce) {
switch (onceAction) {
case RestartOnceTimerIfNotPaused:
if (isPaused())
break;
/* fall through */
case RestartOnceTimer:
scheduleAnimationPolicyTimer();
break;
case CancelOnceTimer:
cancelAnimationPolicyTimer();
break;
}
}
if (policy == ImageAnimationPolicyAllowed) {
// When the SVG owner element becomes detached from its document,
// the policy defaults to ImageAnimationPolicyAllowed; there's
// no way back. If the policy had been "once" prior to that,
// ensure cancellation of its timer.
if (onceAction == CancelOnceTimer)
cancelAnimationPolicyTimer();
}
return true;
}
void SMILTimeContainer::updateDocumentOrderIndexes() {
unsigned timingElementCount = 0;
for (SVGSMILElement& element :
Traversal<SVGSMILElement>::descendantsOf(ownerSVGElement()))
element.setDocumentOrderIndex(timingElementCount++);
m_documentOrderIndexesDirty = false;
}
struct PriorityCompare {
PriorityCompare(double elapsed) : m_elapsed(elapsed) {}
bool operator()(const Member<SVGSMILElement>& a,
const Member<SVGSMILElement>& b) {
// FIXME: This should also consider possible timing relations between the
// elements.
SMILTime aBegin = a->intervalBegin();
SMILTime bBegin = b->intervalBegin();
// Frozen elements need to be prioritized based on their previous interval.
aBegin = a->isFrozen() && m_elapsed < aBegin ? a->previousIntervalBegin()
: aBegin;
bBegin = b->isFrozen() && m_elapsed < bBegin ? b->previousIntervalBegin()
: bBegin;
if (aBegin == bBegin)
return a->documentOrderIndex() < b->documentOrderIndex();
return aBegin < bBegin;
}
double m_elapsed;
};
SVGSVGElement& SMILTimeContainer::ownerSVGElement() const {
return *m_ownerSVGElement;
}
Document& SMILTimeContainer::document() const {
return ownerSVGElement().document();
}
void SMILTimeContainer::serviceOnNextFrame() {
if (document().view()) {
document().view()->scheduleAnimation();
m_frameSchedulingState = AnimationFrame;
}
}
void SMILTimeContainer::serviceAnimations() {
if (m_frameSchedulingState != AnimationFrame)
return;
m_frameSchedulingState = Idle;
updateAnimationsAndScheduleFrameIfNeeded(elapsed());
}
bool SMILTimeContainer::canScheduleFrame(SMILTime earliestFireTime) const {
// If there's synchronization pending (most likely due to syncbases), then
// let that complete first before attempting to schedule a frame.
if (hasPendingSynchronization())
return false;
if (!isTimelineRunning())
return false;
return earliestFireTime.isFinite();
}
void SMILTimeContainer::updateAnimationsAndScheduleFrameIfNeeded(
double elapsed,
bool seekToTime) {
if (!document().isActive())
return;
SMILTime earliestFireTime = updateAnimations(elapsed, seekToTime);
if (!canScheduleFrame(earliestFireTime))
return;
double delayTime = earliestFireTime.value() - elapsed;
scheduleAnimationFrame(delayTime);
}
SMILTime SMILTimeContainer::updateAnimations(double elapsed, bool seekToTime) {
ASSERT(document().isActive());
SMILTime earliestFireTime = SMILTime::unresolved();
#if ENABLE(ASSERT)
// This boolean will catch any attempts to schedule/unschedule
// scheduledAnimations during this critical section. Similarly, any elements
// removed will unschedule themselves, so this will catch modification of
// animationsToApply.
m_preventScheduledAnimationsChanges = true;
#endif
if (m_documentOrderIndexesDirty)
updateDocumentOrderIndexes();
HeapHashSet<ElementAttributePair> invalidKeys;
using AnimationsVector = HeapVector<Member<SVGSMILElement>>;
AnimationsVector animationsToApply;
AnimationsVector scheduledAnimationsInSameGroup;
for (const auto& entry : m_scheduledAnimations) {
if (!entry.key.first || entry.value->isEmpty()) {
invalidKeys.add(entry.key);
continue;
}
// Sort according to priority. Elements with later begin time have higher
// priority. In case of a tie, document order decides.
// FIXME: This should also consider timing relationships between the
// elements. Dependents have higher priority.
copyToVector(*entry.value, scheduledAnimationsInSameGroup);
std::sort(scheduledAnimationsInSameGroup.begin(),
scheduledAnimationsInSameGroup.end(), PriorityCompare(elapsed));
AnimationsVector sandwich;
for (const auto& itAnimation : scheduledAnimationsInSameGroup) {
SVGSMILElement* animation = itAnimation.get();
ASSERT(animation->timeContainer() == this);
ASSERT(animation->targetElement());
ASSERT(animation->hasValidAttributeName());
ASSERT(animation->hasValidAttributeType());
// This will calculate the contribution from the animation and update
// timing.
if (animation->progress(elapsed, seekToTime)) {
sandwich.append(animation);
} else {
animation->clearAnimatedType();
}
SMILTime nextFireTime = animation->nextProgressTime();
if (nextFireTime.isFinite())
earliestFireTime = std::min(nextFireTime, earliestFireTime);
}
if (!sandwich.isEmpty()) {
// Results are accumulated to the first animation that animates and
// contributes to a particular element/attribute pair.
// Only reset the animated type to the base value once for
// the lowest priority animation that animates and
// contributes to a particular element/attribute pair.
SVGSMILElement* resultElement = sandwich.first();
resultElement->resetAnimatedType();
// Go through the sandwich from lowest prio to highest and generate
// the animated value (if any.)
for (const auto& animation : sandwich)
animation->updateAnimatedValue(resultElement);
animationsToApply.append(resultElement);
}
}
m_scheduledAnimations.removeAll(invalidKeys);
if (animationsToApply.isEmpty()) {
#if ENABLE(ASSERT)
m_preventScheduledAnimationsChanges = false;
#endif
return earliestFireTime;
}
UseCounter::count(&document(), UseCounter::SVGSMILAnimationAppliedEffect);
std::sort(animationsToApply.begin(), animationsToApply.end(),
PriorityCompare(elapsed));
// Apply results to target elements.
for (const auto& timedElement : animationsToApply)
timedElement->applyResultsToTarget();
#if ENABLE(ASSERT)
m_preventScheduledAnimationsChanges = false;
#endif
for (const auto& timedElement : animationsToApply) {
if (timedElement->isConnected() && timedElement->isSVGDiscardElement()) {
SVGElement* targetElement = timedElement->targetElement();
if (targetElement && targetElement->isConnected()) {
targetElement->remove(IGNORE_EXCEPTION);
DCHECK(!targetElement->isConnected());
}
if (timedElement->isConnected()) {
timedElement->remove(IGNORE_EXCEPTION);
DCHECK(!timedElement->isConnected());
}
}
}
return earliestFireTime;
}
void SMILTimeContainer::advanceFrameForTesting() {
setElapsed(elapsed() + initialFrameDelay);
}
DEFINE_TRACE(SMILTimeContainer) {
visitor->trace(m_scheduledAnimations);
visitor->trace(m_ownerSVGElement);
}
} // namespace blink