blob: 9d4ef7b16c76247584f7943cdb7b8aa6b3e83de9 [file] [log] [blame]
/*
* Copyright (C) 2013 Google 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:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * 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.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "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 THE COPYRIGHT
* OWNER 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 "config.h"
#include "core/animation/css/CSSAnimations.h"
#include "core/animation/ActiveAnimations.h"
#include "core/animation/DocumentTimeline.h"
#include "core/animation/KeyframeAnimationEffect.h"
#include "core/css/CSSKeyframeRule.h"
#include "core/css/resolver/StyleResolver.h"
#include "core/dom/Element.h"
#include "core/dom/EventNames.h"
#include "core/dom/WebKitAnimationEvent.h"
#include "core/platform/animation/CSSAnimationDataList.h"
#include "core/platform/animation/TimingFunction.h"
#include "wtf/HashSet.h"
namespace {
using namespace WebCore;
bool isEarlierPhase(TimedItem::Phase target, TimedItem::Phase reference)
{
ASSERT(target != TimedItem::PhaseNone);
ASSERT(reference != TimedItem::PhaseNone);
return target < reference;
}
bool isLaterPhase(TimedItem::Phase target, TimedItem::Phase reference)
{
ASSERT(target != TimedItem::PhaseNone);
ASSERT(reference != TimedItem::PhaseNone);
return target > reference;
}
} // namespace
namespace WebCore {
// Returns the default timing function.
const PassRefPtr<TimingFunction> timingFromAnimationData(const CSSAnimationData* animationData, Timing& timing)
{
if (animationData->isDelaySet())
timing.startDelay = animationData->delay();
if (animationData->isDurationSet()) {
timing.iterationDuration = animationData->duration();
timing.hasIterationDuration = true;
}
if (animationData->isIterationCountSet()) {
if (animationData->iterationCount() == CSSAnimationData::IterationCountInfinite)
timing.iterationCount = std::numeric_limits<double>::infinity();
else
timing.iterationCount = animationData->iterationCount();
}
if (animationData->isFillModeSet()) {
switch (animationData->fillMode()) {
case AnimationFillModeForwards:
timing.fillMode = Timing::FillModeForwards;
break;
case AnimationFillModeBackwards:
timing.fillMode = Timing::FillModeBackwards;
break;
case AnimationFillModeBoth:
timing.fillMode = Timing::FillModeBoth;
break;
case AnimationFillModeNone:
timing.fillMode = Timing::FillModeNone;
break;
default:
ASSERT_NOT_REACHED();
}
}
if (animationData->isDirectionSet()) {
switch (animationData->direction()) {
case CSSAnimationData::AnimationDirectionNormal:
timing.direction = Timing::PlaybackDirectionNormal;
break;
case CSSAnimationData::AnimationDirectionAlternate:
timing.direction = Timing::PlaybackDirectionAlternate;
break;
case CSSAnimationData::AnimationDirectionReverse:
timing.direction = Timing::PlaybackDirectionReverse;
break;
case CSSAnimationData::AnimationDirectionAlternateReverse:
timing.direction = Timing::PlaybackDirectionAlternateReverse;
break;
default:
ASSERT_NOT_REACHED();
}
}
return animationData->isTimingFunctionSet() ? animationData->timingFunction() : CSSAnimationData::initialAnimationTimingFunction();
}
CSSAnimationUpdateScope::CSSAnimationUpdateScope(Element* target)
: m_target(target)
{
if (!m_target)
return;
ActiveAnimations* activeAnimations = m_target->activeAnimations();
CSSAnimations* cssAnimations = activeAnimations ? activeAnimations->cssAnimations() : 0;
// It's possible than an update was created outside an update scope. That's harmless
// but we must clear it now to avoid applying it if an updated replacement is not
// created in this scope.
if (cssAnimations)
cssAnimations->setPendingUpdate(nullptr);
}
CSSAnimationUpdateScope::~CSSAnimationUpdateScope()
{
if (!m_target)
return;
ActiveAnimations* activeAnimations = m_target->activeAnimations();
CSSAnimations* cssAnimations = activeAnimations ? activeAnimations->cssAnimations() : 0;
if (cssAnimations)
cssAnimations->maybeApplyPendingUpdate(m_target);
}
bool CSSAnimations::needsUpdate(const Element* element, const RenderStyle* style)
{
ActiveAnimations* activeAnimations = element->activeAnimations();
const CSSAnimationDataList* animations = style->animations();
const CSSAnimations* cssAnimations = activeAnimations ? activeAnimations->cssAnimations() : 0;
EDisplay display = style->display();
return (display != NONE && animations && animations->size()) || (cssAnimations && !cssAnimations->isEmpty());
}
PassOwnPtr<CSSAnimationUpdate> CSSAnimations::calculateUpdate(const Element* element, const RenderStyle* style, const CSSAnimations* cssAnimations, const CSSAnimationDataList* animationDataList, StyleResolver* resolver)
{
OwnPtr<CSSAnimationUpdate> update;
HashSet<AtomicString> inactive;
if (cssAnimations)
for (AnimationMap::const_iterator iter = cssAnimations->m_animations.begin(); iter != cssAnimations->m_animations.end(); ++iter)
inactive.add(iter->key);
RefPtr<MutableStylePropertySet> newStyles;
if (style->display() != NONE) {
for (size_t i = 0; animationDataList && i < animationDataList->size(); ++i) {
const CSSAnimationData* animationData = animationDataList->animation(i);
if (animationData->isNoneAnimation())
continue;
ASSERT(animationData->isValidAnimation());
AtomicString animationName(animationData->name());
if (cssAnimations) {
AnimationMap::const_iterator existing(cssAnimations->m_animations.find(animationName));
if (existing != cssAnimations->m_animations.end()) {
// FIXME: The play-state of this animation might have changed, record the change in the update.
inactive.remove(animationName);
continue;
}
}
Timing timing;
RefPtr<TimingFunction> defaultTimingFunction = timingFromAnimationData(animationData, timing);
KeyframeAnimationEffect::KeyframeVector keyframes;
resolver->resolveKeyframes(element, style, animationName, defaultTimingFunction.get(), keyframes, timing.timingFunction);
if (!keyframes.isEmpty()) {
if (!update)
update = adoptPtr(new CSSAnimationUpdate());
// FIXME: crbug.com/268791 - Keyframes are already normalized, perhaps there should be a flag on KeyframeAnimationEffect to skip normalization.
update->startAnimation(animationName, InertAnimation::create(KeyframeAnimationEffect::create(keyframes), timing).get());
}
}
}
if (!inactive.isEmpty() && !update)
update = adoptPtr(new CSSAnimationUpdate());
for (HashSet<AtomicString>::const_iterator iter = inactive.begin(); iter != inactive.end(); ++iter)
update->cancelAnimation(*iter, cssAnimations->m_animations.get(*iter));
return update.release();
}
void CSSAnimations::maybeApplyPendingUpdate(Element* element)
{
if (!element->renderer())
m_pendingUpdate = nullptr;
if (!m_pendingUpdate)
return;
OwnPtr<CSSAnimationUpdate> update = m_pendingUpdate.release();
for (Vector<AtomicString>::const_iterator iter = update->cancelledAnimationNames().begin(); iter != update->cancelledAnimationNames().end(); ++iter)
m_animations.take(*iter)->cancel();
// FIXME: Apply updates to play-state.
for (Vector<CSSAnimationUpdate::NewAnimation>::const_iterator iter = update->newAnimations().begin(); iter != update->newAnimations().end(); ++iter) {
OwnPtr<CSSAnimations::EventDelegate> eventDelegate = adoptPtr(new EventDelegate(element, iter->name));
RefPtr<Animation> animation = Animation::create(element, iter->animation->effect(), iter->animation->specified(), eventDelegate.release());
RefPtr<Player> player = element->document().timeline()->play(animation.get());
m_animations.set(iter->name, player.get());
}
}
void CSSAnimations::cancel()
{
for (AnimationMap::iterator iter = m_animations.begin(); iter != m_animations.end(); ++iter)
iter->value->cancel();
m_animations.clear();
m_pendingUpdate = nullptr;
}
void CSSAnimations::EventDelegate::maybeDispatch(Document::ListenerType listenerType, AtomicString& eventName, double elapsedTime)
{
if (m_target->document().hasListenerType(listenerType))
m_target->document().timeline()->addEventToDispatch(m_target, WebKitAnimationEvent::create(eventName, m_name, elapsedTime));
}
void CSSAnimations::EventDelegate::onEventCondition(const TimedItem* timedItem, bool isFirstSample, TimedItem::Phase previousPhase, double previousIteration)
{
// Events for a single document are queued and dispatched as a group at
// the end of DocumentTimeline::serviceAnimations.
// FIXME: Events which are queued outside of serviceAnimations should
// trigger a timer to dispatch when control is released.
const TimedItem::Phase currentPhase = timedItem->phase();
const double currentIteration = timedItem->currentIteration();
// Note that the elapsedTime is measured from when the animation starts playing.
if (!isFirstSample && previousPhase == TimedItem::PhaseActive && currentPhase == TimedItem::PhaseActive && previousIteration != currentIteration) {
ASSERT(!isNull(previousIteration));
ASSERT(!isNull(currentIteration));
// We fire only a single event for all iterations thast terminate
// between a single pair of samples. See http://crbug.com/275263. For
// compatibility with the existing implementation, this event uses
// the elapsedTime for the first iteration in question.
ASSERT(timedItem->specified().hasIterationDuration);
const double elapsedTime = timedItem->specified().iterationDuration * (previousIteration + 1);
maybeDispatch(Document::ANIMATIONITERATION_LISTENER, eventNames().animationiterationEvent, elapsedTime);
return;
}
if ((isFirstSample || previousPhase == TimedItem::PhaseBefore) && isLaterPhase(currentPhase, TimedItem::PhaseBefore)) {
ASSERT(timedItem->specified().startDelay > 0 || isFirstSample);
// The spec states that the elapsed time should be
// 'delay < 0 ? -delay : 0', but we always use 0 to match the existing
// implementation. See crbug.com/279611
maybeDispatch(Document::ANIMATIONSTART_LISTENER, eventNames().animationstartEvent, 0);
}
if ((isFirstSample || isEarlierPhase(previousPhase, TimedItem::PhaseAfter)) && currentPhase == TimedItem::PhaseAfter)
maybeDispatch(Document::ANIMATIONEND_LISTENER, eventNames().animationendEvent, timedItem->activeDuration());
}
bool CSSAnimations::isAnimatableProperty(CSSPropertyID property)
{
switch (property) {
case CSSPropertyBackgroundColor:
case CSSPropertyBackgroundImage:
case CSSPropertyBackgroundPositionX:
case CSSPropertyBackgroundPositionY:
case CSSPropertyBackgroundSize:
case CSSPropertyBaselineShift:
case CSSPropertyBorderBottomColor:
case CSSPropertyBorderBottomLeftRadius:
case CSSPropertyBorderBottomRightRadius:
case CSSPropertyBorderBottomWidth:
case CSSPropertyBorderImageOutset:
case CSSPropertyBorderImageSlice:
case CSSPropertyBorderImageSource:
case CSSPropertyBorderImageWidth:
case CSSPropertyBorderLeftColor:
case CSSPropertyBorderLeftWidth:
case CSSPropertyBorderRightColor:
case CSSPropertyBorderRightWidth:
case CSSPropertyBorderTopColor:
case CSSPropertyBorderTopLeftRadius:
case CSSPropertyBorderTopRightRadius:
case CSSPropertyBorderTopWidth:
case CSSPropertyBottom:
case CSSPropertyBoxShadow:
case CSSPropertyClip:
case CSSPropertyColor:
case CSSPropertyFill:
case CSSPropertyFillOpacity:
case CSSPropertyFlex:
case CSSPropertyFloodColor:
case CSSPropertyFloodOpacity:
case CSSPropertyFontSize:
case CSSPropertyHeight:
case CSSPropertyKerning:
case CSSPropertyLeft:
case CSSPropertyLetterSpacing:
case CSSPropertyLightingColor:
case CSSPropertyLineHeight:
case CSSPropertyListStyleImage:
case CSSPropertyMarginBottom:
case CSSPropertyMarginLeft:
case CSSPropertyMarginRight:
case CSSPropertyMarginTop:
case CSSPropertyMaxHeight:
case CSSPropertyMaxWidth:
case CSSPropertyMinHeight:
case CSSPropertyMinWidth:
case CSSPropertyOpacity:
case CSSPropertyOrphans:
case CSSPropertyOutlineColor:
case CSSPropertyOutlineOffset:
case CSSPropertyOutlineWidth:
case CSSPropertyPaddingBottom:
case CSSPropertyPaddingLeft:
case CSSPropertyPaddingRight:
case CSSPropertyPaddingTop:
case CSSPropertyRight:
case CSSPropertyStopColor:
case CSSPropertyStopOpacity:
case CSSPropertyStroke:
case CSSPropertyStrokeDasharray:
case CSSPropertyStrokeDashoffset:
case CSSPropertyStrokeMiterlimit:
case CSSPropertyStrokeOpacity:
case CSSPropertyStrokeWidth:
case CSSPropertyTextIndent:
case CSSPropertyTextShadow:
case CSSPropertyTop:
case CSSPropertyVisibility:
case CSSPropertyWebkitBackgroundSize:
case CSSPropertyWebkitBorderHorizontalSpacing:
case CSSPropertyWebkitBorderVerticalSpacing:
case CSSPropertyWebkitBoxShadow:
case CSSPropertyWebkitClipPath:
case CSSPropertyWebkitColumnCount:
case CSSPropertyWebkitColumnGap:
case CSSPropertyWebkitColumnRuleColor:
case CSSPropertyWebkitColumnRuleWidth:
case CSSPropertyWebkitColumnWidth:
case CSSPropertyWebkitFilter:
case CSSPropertyWebkitMaskBoxImage:
case CSSPropertyWebkitMaskBoxImageSource:
case CSSPropertyWebkitMaskImage:
case CSSPropertyWebkitMaskPositionX:
case CSSPropertyWebkitMaskPositionY:
case CSSPropertyWebkitMaskSize:
case CSSPropertyWebkitPerspective:
case CSSPropertyWebkitPerspectiveOriginX:
case CSSPropertyWebkitPerspectiveOriginY:
case CSSPropertyWebkitShapeInside:
case CSSPropertyWebkitTextFillColor:
case CSSPropertyWebkitTextStrokeColor:
case CSSPropertyWebkitTransform:
case CSSPropertyWebkitTransformOriginX:
case CSSPropertyWebkitTransformOriginY:
case CSSPropertyWebkitTransformOriginZ:
case CSSPropertyWidows:
case CSSPropertyWidth:
case CSSPropertyWordSpacing:
case CSSPropertyZIndex:
case CSSPropertyZoom:
return true;
default:
return false;
}
}
} // namespace WebCore