blob: 1aa8dbc7bb0f5b0780bc741174d705580a871dd1 [file] [log] [blame]
/*
* Copyright (C) Canon Inc. 2016
* Copyright (C) 2017 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 "config.h"
#include "AnimationTimeline.h"
#include "Animation.h"
#include "AnimationEffectReadOnly.h"
#include "AnimationList.h"
#include "CSSAnimation.h"
#include "CSSPropertyAnimation.h"
#include "CSSTransition.h"
#include "DocumentTimeline.h"
#include "Element.h"
#include "KeyframeEffectReadOnly.h"
#include "RenderStyle.h"
#include "RenderView.h"
#include "WebAnimationUtilities.h"
#include <wtf/text/TextStream.h>
#include <wtf/text/WTFString.h>
namespace WebCore {
AnimationTimeline::AnimationTimeline(ClassType classType)
: m_classType(classType)
{
}
AnimationTimeline::~AnimationTimeline()
{
m_animations.clear();
m_elementToAnimationsMap.clear();
m_elementToCSSAnimationsMap.clear();
m_elementToCSSTransitionsMap.clear();
m_elementToCSSAnimationByName.clear();
m_elementToCSSTransitionByCSSPropertyID.clear();
}
void AnimationTimeline::addAnimation(Ref<WebAnimation>&& animation)
{
m_animations.add(WTFMove(animation));
timingModelDidChange();
}
void AnimationTimeline::removeAnimation(Ref<WebAnimation>&& animation)
{
m_animations.remove(WTFMove(animation));
timingModelDidChange();
}
std::optional<double> AnimationTimeline::bindingsCurrentTime()
{
auto time = currentTime();
if (!time)
return std::nullopt;
return secondsToWebAnimationsAPITime(*time);
}
void AnimationTimeline::setCurrentTime(Seconds currentTime)
{
m_currentTime = currentTime;
timingModelDidChange();
}
HashMap<Element*, Vector<RefPtr<WebAnimation>>>& AnimationTimeline::relevantMapForAnimation(WebAnimation& animation)
{
if (animation.isCSSAnimation())
return m_elementToCSSAnimationsMap;
if (animation.isCSSTransition())
return m_elementToCSSTransitionsMap;
return m_elementToAnimationsMap;
}
void AnimationTimeline::animationWasAddedToElement(WebAnimation& animation, Element& element)
{
auto result = relevantMapForAnimation(animation).ensure(&element, [] {
return Vector<RefPtr<WebAnimation>> { };
});
result.iterator->value.append(&animation);
}
void AnimationTimeline::animationWasRemovedFromElement(WebAnimation& animation, Element& element)
{
auto& map = relevantMapForAnimation(animation);
auto iterator = map.find(&element);
if (iterator == map.end())
return;
auto& animations = iterator->value;
animations.removeFirst(&animation);
if (!animations.size())
map.remove(iterator);
}
Vector<RefPtr<WebAnimation>> AnimationTimeline::animationsForElement(Element& element) const
{
Vector<RefPtr<WebAnimation>> animations;
if (m_elementToCSSAnimationsMap.contains(&element))
animations.appendVector(m_elementToCSSAnimationsMap.get(&element));
if (m_elementToCSSTransitionsMap.contains(&element))
animations.appendVector(m_elementToCSSTransitionsMap.get(&element));
if (m_elementToAnimationsMap.contains(&element))
animations.appendVector(m_elementToAnimationsMap.get(&element));
return animations;
}
void AnimationTimeline::cancelDeclarativeAnimationsForElement(Element& element)
{
for (const auto& animation : animationsForElement(element)) {
if (is<DeclarativeAnimation>(animation))
animation->cancel();
}
}
void AnimationTimeline::updateCSSAnimationsForElement(Element& element, const RenderStyle& newStyle, const RenderStyle* oldStyle)
{
if (element.document().pageCacheState() != Document::NotInPageCache)
return;
if (element.document().renderView()->printing())
return;
// In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
if (oldStyle && oldStyle->hasAnimations() && oldStyle->display() != NONE && newStyle.display() == NONE) {
if (m_elementToCSSAnimationByName.contains(&element)) {
for (const auto& cssAnimationsByNameMapItem : m_elementToCSSAnimationByName.take(&element))
cancelOrRemoveDeclarativeAnimation(cssAnimationsByNameMapItem.value);
}
return;
}
if (oldStyle && oldStyle->hasAnimations() && newStyle.hasAnimations() && *(oldStyle->animations()) == *(newStyle.animations()))
return;
// First, compile the list of animation names that were applied to this element up to this point.
HashSet<String> namesOfPreviousAnimations;
if (oldStyle && oldStyle->hasAnimations()) {
auto* previousAnimations = oldStyle->animations();
for (size_t i = 0; i < previousAnimations->size(); ++i) {
auto& previousAnimation = previousAnimations->animation(i);
if (previousAnimation.isValidAnimation())
namesOfPreviousAnimations.add(previousAnimation.name());
}
}
// Create or get the CSSAnimations by animation name map for this element.
auto& cssAnimationsByName = m_elementToCSSAnimationByName.ensure(&element, [] {
return HashMap<String, RefPtr<CSSAnimation>> { };
}).iterator->value;
if (auto* currentAnimations = newStyle.animations()) {
for (size_t i = 0; i < currentAnimations->size(); ++i) {
auto& currentAnimation = currentAnimations->animation(i);
auto& name = currentAnimation.name();
if (namesOfPreviousAnimations.contains(name)) {
// We've found the name of this animation in our list of previous animations, this means we've already
// created a CSSAnimation object for it and need to ensure that this CSSAnimation is backed by the current
// animation object for this animation name.
cssAnimationsByName.get(name)->setBackingAnimation(currentAnimation);
} else if (currentAnimation.isValidAnimation()) {
// Otherwise we are dealing with a new animation name and must create a CSSAnimation for it.
cssAnimationsByName.set(name, CSSAnimation::create(element, currentAnimation, oldStyle, newStyle));
}
// Remove the name of this animation from our list since it's now known to be current.
namesOfPreviousAnimations.remove(name);
}
}
// The animations names left in namesOfPreviousAnimations are now known to no longer apply so we need to
// remove the CSSAnimation object created for them.
for (const auto& nameOfAnimationToRemove : namesOfPreviousAnimations)
cancelOrRemoveDeclarativeAnimation(cssAnimationsByName.take(nameOfAnimationToRemove));
// Remove the map of CSSAnimations by animation name for this element if it's now empty.
if (cssAnimationsByName.isEmpty())
m_elementToCSSAnimationByName.remove(&element);
}
RefPtr<WebAnimation> AnimationTimeline::cssAnimationForElementAndProperty(Element& element, CSSPropertyID property)
{
RefPtr<WebAnimation> matchingAnimation;
for (const auto& animation : m_elementToCSSAnimationsMap.get(&element)) {
auto* effect = animation->effect();
if (is<KeyframeEffectReadOnly>(effect) && downcast<KeyframeEffectReadOnly>(effect)->animatedProperties().contains(property))
matchingAnimation = animation;
}
return matchingAnimation;
}
static bool shouldBackingAnimationBeConsideredForCSSTransition(const Animation& backingAnimation)
{
auto mode = backingAnimation.animationMode();
if (mode == Animation::AnimateNone || mode == Animation::AnimateUnknownProperty)
return false;
if (mode == Animation::AnimateSingleProperty && backingAnimation.property() == CSSPropertyInvalid)
return false;
return true;
}
void AnimationTimeline::updateCSSTransitionsForElement(Element& element, const RenderStyle& newStyle, const RenderStyle* oldStyle)
{
if (element.document().pageCacheState() != Document::NotInPageCache)
return;
if (element.document().renderView()->printing())
return;
// In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
if (oldStyle && oldStyle->hasTransitions() && oldStyle->display() != NONE && newStyle.display() == NONE) {
if (m_elementToCSSTransitionByCSSPropertyID.contains(&element)) {
for (const auto& cssTransitionsByCSSPropertyIDMapItem : m_elementToCSSTransitionByCSSPropertyID.take(&element))
cancelOrRemoveDeclarativeAnimation(cssTransitionsByCSSPropertyIDMapItem.value);
}
return;
}
// Create or get the CSSTransitions by CSS property name map for this element.
auto& cssTransitionsByProperty = m_elementToCSSTransitionByCSSPropertyID.ensure(&element, [] {
return HashMap<CSSPropertyID, RefPtr<CSSTransition>> { };
}).iterator->value;
// First, compile the list of backing animations and properties that were applied to this element up to this point.
auto previousProperties = copyToVector(cssTransitionsByProperty.keys());
HashSet<const Animation*> previousBackingAnimations;
if (oldStyle && oldStyle->hasTransitions()) {
auto* previousTransitions = oldStyle->transitions();
for (size_t i = 0; i < previousTransitions->size(); ++i) {
auto& backingAnimation = previousTransitions->animation(i);
if (shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
previousBackingAnimations.add(&backingAnimation);
}
}
if (auto* currentTransitions = newStyle.transitions()) {
for (size_t i = 0; i < currentTransitions->size(); ++i) {
auto& backingAnimation = currentTransitions->animation(i);
if (!shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
continue;
auto property = backingAnimation.property();
bool transitionsAllProperties = backingAnimation.animationMode() == Animation::AnimateAll;
auto numberOfProperties = CSSPropertyAnimation::getNumProperties();
// In the "transition-property: all" case, where the animation's mode is set to AnimateAll,
// the property will be set to CSSPropertyInvalid and we need to iterate over all known
// CSS properties and see if they have mis-matching values in the old and new styles, which
// means they should have a CSSTransition created for them.
// We implement a single loop which handles the "all" case and the specified property case
// by using the pre-set property above in the specified property case and breaking out of
// the loop after the first complete iteration.
for (int propertyIndex = 0; propertyIndex < numberOfProperties; ++propertyIndex) {
if (transitionsAllProperties) {
bool isShorthand;
property = CSSPropertyAnimation::getPropertyAtIndex(propertyIndex, isShorthand);
if (isShorthand)
continue;
} else if (propertyIndex) {
// We only go once through this loop if we are transitioning a single property.
break;
}
previousProperties.removeFirst(property);
// We've found a backing animation that we didn't know about for a valid property.
if (!previousBackingAnimations.contains(&backingAnimation)) {
// If we already had a CSSTransition for this property, check whether its timing properties match the current backing
// animation's properties and whether its blending keyframes match the old and new styles. If they do, move on to the
// next transition, otherwise delete the previous CSSTransition object, and create a new one.
if (cssTransitionsByProperty.contains(property)) {
if (cssTransitionsByProperty.get(property)->matchesBackingAnimationAndStyles(backingAnimation, oldStyle, newStyle))
continue;
removeDeclarativeAnimation(cssTransitionsByProperty.take(property));
}
// Now we can create a new CSSTransition with the new backing animation provided it has a valid
// duration and the from and to values are distinct.
if ((backingAnimation.duration() || backingAnimation.delay() > 0) && oldStyle) {
auto existingAnimation = cssAnimationForElementAndProperty(element, property);
const auto* fromStyle = existingAnimation ? &downcast<CSSAnimation>(existingAnimation.get())->unanimatedStyle() : oldStyle;
if (!CSSPropertyAnimation::propertiesEqual(property, fromStyle, &newStyle))
cssTransitionsByProperty.set(property, CSSTransition::create(element, property, backingAnimation, fromStyle, newStyle));
}
}
}
}
}
// Remaining properties are no longer current and must be removed.
for (const auto transitionPropertyToRemove : previousProperties) {
if (cssTransitionsByProperty.contains(transitionPropertyToRemove))
cancelOrRemoveDeclarativeAnimation(cssTransitionsByProperty.take(transitionPropertyToRemove));
}
// Remove the map of CSSTransitions by property for this element if it's now empty.
if (cssTransitionsByProperty.isEmpty())
m_elementToCSSTransitionByCSSPropertyID.remove(&element);
}
void AnimationTimeline::removeDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
{
animation->setEffect(nullptr);
removeAnimation(animation.releaseNonNull());
}
void AnimationTimeline::cancelOrRemoveDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
{
auto phase = animation->effect()->phase();
if (phase != AnimationEffectReadOnly::Phase::Idle && phase != AnimationEffectReadOnly::Phase::After)
animation->cancel();
else
removeDeclarativeAnimation(animation);
}
String AnimationTimeline::description()
{
TextStream stream;
int count = 1;
stream << (m_classType == DocumentTimelineClass ? "DocumentTimeline" : "AnimationTimeline") << " with " << m_animations.size() << " animations:";
stream << "\n";
for (const auto& animation : m_animations) {
writeIndent(stream, 1);
stream << count << ". " << animation->description();
stream << "\n";
count++;
}
return stream.release();
}
} // namespace WebCore