AnimationTimeline.cpp [plain text]
#include "config.h"
#include "AnimationTimeline.h"
#include "Animation.h"
#include "AnimationEffect.h"
#include "AnimationList.h"
#include "CSSAnimation.h"
#include "CSSPropertyAnimation.h"
#include "CSSTransition.h"
#include "DocumentTimeline.h"
#include "Element.h"
#include "KeyframeEffect.h"
#include "KeyframeEffectStack.h"
#include "PseudoElement.h"
#include "RenderStyle.h"
#include "RenderView.h"
#include "StylePropertyShorthand.h"
#include "StyleResolver.h"
#include "WebAnimationUtilities.h"
#include <wtf/text/TextStream.h>
#include <wtf/text/WTFString.h>
namespace WebCore {
AnimationTimeline::AnimationTimeline() = default;
AnimationTimeline::~AnimationTimeline() = default;
void AnimationTimeline::forgetAnimation(WebAnimation* animation)
{
m_allAnimations.removeFirst(animation);
}
void AnimationTimeline::animationTimingDidChange(WebAnimation& animation)
{
updateGlobalPosition(animation);
if (m_animations.add(&animation)) {
m_allAnimations.append(makeWeakPtr(&animation));
auto* timeline = animation.timeline();
if (timeline && timeline != this)
timeline->removeAnimation(animation);
}
}
void AnimationTimeline::updateGlobalPosition(WebAnimation& animation)
{
static uint64_t s_globalPosition = 0;
if (!animation.globalPosition() && animation.canHaveGlobalPosition())
animation.setGlobalPosition(++s_globalPosition);
}
void AnimationTimeline::removeAnimation(WebAnimation& animation)
{
ASSERT(!animation.timeline() || animation.timeline() == this);
m_animations.remove(&animation);
if (is<KeyframeEffect>(animation.effect())) {
if (auto* target = downcast<KeyframeEffect>(animation.effect())->targetElementOrPseudoElement()) {
animationWasRemovedFromElement(animation, *target);
target->ensureKeyframeEffectStack().removeEffect(*downcast<KeyframeEffect>(animation.effect()));
}
}
}
Optional<double> AnimationTimeline::bindingsCurrentTime()
{
auto time = currentTime();
if (!time)
return WTF::nullopt;
return secondsToWebAnimationsAPITime(*time);
}
void AnimationTimeline::animationWasAddedToElement(WebAnimation& animation, Element& element)
{
if (is<CSSTransition>(animation) && downcast<CSSTransition>(animation).owningElement() == &element)
element.ensureTransitions().add(&animation);
else if (is<CSSAnimation>(animation) && downcast<CSSAnimation>(animation).owningElement() == &element)
element.ensureCSSAnimations().add(&animation);
else
element.ensureWebAnimations().add(&animation);
}
static inline bool removeCSSTransitionFromMap(CSSTransition& transition, PropertyToTransitionMap& cssTransitionsByProperty)
{
auto transitionIterator = cssTransitionsByProperty.find(transition.property());
if (transitionIterator == cssTransitionsByProperty.end() || transitionIterator->value != &transition)
return false;
cssTransitionsByProperty.remove(transitionIterator);
return true;
}
void AnimationTimeline::animationWasRemovedFromElement(WebAnimation& animation, Element& element)
{
element.ensureTransitions().remove(&animation);
element.ensureCSSAnimations().remove(&animation);
element.ensureWebAnimations().remove(&animation);
if (is<CSSTransition>(animation))
removeDeclarativeAnimationFromListsForOwningElement(animation, element);
}
void AnimationTimeline::removeDeclarativeAnimationFromListsForOwningElement(WebAnimation& animation, Element& element)
{
ASSERT(is<DeclarativeAnimation>(animation));
if (is<CSSTransition>(animation)) {
auto& transition = downcast<CSSTransition>(animation);
if (!removeCSSTransitionFromMap(transition, element.ensureRunningTransitionsByProperty()))
removeCSSTransitionFromMap(transition, element.ensureCompletedTransitionsByProperty());
}
}
Vector<RefPtr<WebAnimation>> AnimationTimeline::animationsForElement(Element& element, Ordering ordering) const
{
Vector<RefPtr<WebAnimation>> animations;
if (ordering == Ordering::Sorted) {
if (element.hasKeyframeEffects()) {
for (auto& effect : element.ensureKeyframeEffectStack().sortedEffects())
animations.append(effect->animation());
}
} else {
if (auto* cssTransitions = element.transitions())
animations.appendRange(cssTransitions->begin(), cssTransitions->end());
if (auto* cssAnimations = element.cssAnimations())
animations.appendRange(cssAnimations->begin(), cssAnimations->end());
if (auto* webAnimations = element.webAnimations())
animations.appendRange(webAnimations->begin(), webAnimations->end());
}
return animations;
}
void AnimationTimeline::removeCSSAnimationCreatedByMarkup(Element& element, CSSAnimation& cssAnimation)
{
element.animationsCreatedByMarkup().remove(&cssAnimation);
if (!element.hasKeyframeEffects())
return;
auto& keyframeEffectStack = element.ensureKeyframeEffectStack();
auto* cssAnimationList = keyframeEffectStack.cssAnimationList();
if (!cssAnimationList || cssAnimationList->isEmpty())
return;
auto& backingAnimation = cssAnimation.backingAnimation();
for (size_t i = 0; i < cssAnimationList->size(); ++i) {
if (cssAnimationList->animation(i) == backingAnimation) {
auto newAnimationList = cssAnimationList->copy();
newAnimationList->remove(i);
keyframeEffectStack.setCSSAnimationList(WTFMove(newAnimationList));
return;
}
}
}
void AnimationTimeline::elementWasRemoved(Element& element)
{
cancelDeclarativeAnimationsForElement(element, WebAnimation::Silently::Yes);
}
void AnimationTimeline::willChangeRendererForElement(Element& element)
{
for (auto& animation : animationsForElement(element))
animation->willChangeRenderer();
}
void AnimationTimeline::cancelDeclarativeAnimationsForElement(Element& element, WebAnimation::Silently silently)
{
if (auto* transitions = element.transitions()) {
for (auto& cssTransition : *transitions)
cssTransition->cancel(silently);
}
if (auto* cssAnimations = element.cssAnimations()) {
for (auto& cssAnimation : *cssAnimations) {
if (is<CSSAnimation>(cssAnimation))
removeCSSAnimationCreatedByMarkup(element, downcast<CSSAnimation>(*cssAnimation));
cssAnimation->cancel(silently);
}
}
}
static bool shouldConsiderAnimation(Element& elementOrPseudoElement, const Animation& animation)
{
if (!animation.isValidAnimation())
return false;
static NeverDestroyed<const String> animationNameNone(MAKE_STATIC_STRING_IMPL("none"));
auto& name = animation.name();
if (name == animationNameNone || name.isEmpty())
return false;
auto& element = is<PseudoElement>(elementOrPseudoElement) ? *downcast<PseudoElement>(elementOrPseudoElement).hostElement() : elementOrPseudoElement;
if (auto* styleScope = Style::Scope::forOrdinal(element, animation.nameStyleScopeOrdinal()))
return styleScope->resolver().isAnimationNameValid(name);
return false;
}
void AnimationTimeline::updateCSSAnimationsForElement(Element& element, const RenderStyle* currentStyle, const RenderStyle& newStyle)
{
auto& keyframeEffectStack = element.ensureKeyframeEffectStack();
if (currentStyle && currentStyle->display() != DisplayType::None && newStyle.display() == DisplayType::None) {
for (auto& cssAnimation : element.animationsCreatedByMarkup())
cssAnimation->cancelFromStyle();
keyframeEffectStack.setCSSAnimationList(nullptr);
return;
}
auto* currentAnimationList = newStyle.animations();
auto* previousAnimationList = keyframeEffectStack.cssAnimationList();
if (previousAnimationList && !previousAnimationList->isEmpty() && newStyle.hasAnimations() && *(previousAnimationList) == *(newStyle.animations()))
return;
CSSAnimationCollection newAnimations;
auto& previousAnimations = element.animationsCreatedByMarkup();
if (currentAnimationList) {
for (size_t i = currentAnimationList->size(); i > 0; --i) {
auto& currentAnimation = currentAnimationList->animation(i - 1);
if (!shouldConsiderAnimation(element, currentAnimation))
continue;
bool foundMatchingAnimation = false;
for (auto& previousAnimation : previousAnimations) {
if (previousAnimation->animationName() == currentAnimation.name()) {
previousAnimation->setBackingAnimation(currentAnimation);
newAnimations.add(previousAnimation);
previousAnimations.remove(previousAnimation);
foundMatchingAnimation = true;
break;
}
}
if (!foundMatchingAnimation)
newAnimations.add(CSSAnimation::create(element, currentAnimation, currentStyle, newStyle));
}
}
for (auto& previousAnimation : previousAnimations) {
if (!newAnimations.contains(previousAnimation)) {
if (previousAnimation->owningElement())
previousAnimation->cancelFromStyle();
}
}
element.setAnimationsCreatedByMarkup(WTFMove(newAnimations));
keyframeEffectStack.setCSSAnimationList(currentAnimationList);
}
static KeyframeEffect* keyframeEffectForElementAndProperty(Element& element, CSSPropertyID property)
{
if (auto* keyframeEffectStack = element.keyframeEffectStack()) {
auto effects = keyframeEffectStack->sortedEffects();
for (const auto& effect : makeReversedRange(effects)) {
if (effect->animatesProperty(property))
return effect.get();
}
}
return nullptr;
}
static bool propertyInStyleMatchesValueForTransitionInMap(CSSPropertyID property, const RenderStyle& style, PropertyToTransitionMap& transitions)
{
if (auto* transition = transitions.get(property)) {
if (CSSPropertyAnimation::propertiesEqual(property, &style, &transition->targetStyle()))
return true;
}
return false;
}
static double transitionCombinedDuration(const Animation* transition)
{
return std::max(0.0, transition->duration()) + transition->delay();
}
static bool transitionMatchesProperty(const Animation& transition, CSSPropertyID property)
{
auto mode = transition.property().mode;
if (mode == Animation::TransitionMode::None || mode == Animation::TransitionMode::UnknownProperty)
return false;
if (mode == Animation::TransitionMode::SingleProperty) {
auto transitionProperty = transition.property().id;
if (transitionProperty != property) {
for (auto longhand : shorthandForProperty(transitionProperty)) {
if (longhand == property)
return true;
}
return false;
}
}
return true;
}
static void compileTransitionPropertiesInStyle(const RenderStyle& style, HashSet<CSSPropertyID>& transitionProperties, bool& transitionPropertiesContainAll)
{
if (transitionPropertiesContainAll)
return;
auto* transitions = style.transitions();
if (!transitions)
return;
for (size_t i = 0; i < transitions->size(); ++i) {
const auto& animation = transitions->animation(i);
auto mode = animation.property().mode;
if (mode == Animation::TransitionMode::SingleProperty) {
auto property = animation.property().id;
if (isShorthandCSSProperty(property)) {
for (auto longhand : shorthandForProperty(property))
transitionProperties.add(longhand);
} else if (property != CSSPropertyInvalid)
transitionProperties.add(property);
} else if (mode == Animation::TransitionMode::All) {
transitionPropertiesContainAll = true;
return;
}
}
}
void AnimationTimeline::updateCSSTransitionsForElementAndProperty(Element& element, CSSPropertyID property, const RenderStyle& currentStyle, const RenderStyle& newStyle, const MonotonicTime generationTime)
{
auto* keyframeEffect = keyframeEffectForElementAndProperty(element, property);
auto* animation = keyframeEffect ? keyframeEffect->animation() : nullptr;
bool isDeclarative = animation && is<DeclarativeAnimation>(animation) && downcast<DeclarativeAnimation>(*animation).owningElement() == &element;
if (animation && !isDeclarative)
return;
const Animation* matchingBackingAnimation = nullptr;
if (auto* transitions = newStyle.transitions()) {
for (size_t i = 0; i < transitions->size(); ++i) {
auto& backingAnimation = transitions->animation(i);
if (transitionMatchesProperty(backingAnimation, property))
matchingBackingAnimation = &backingAnimation;
}
}
if (is<CSSTransition>(animation) && matchingBackingAnimation && element.hasRunningTransitionsForProperty(property) && animation->playState() == WebAnimation::PlayState::Finished) {
element.ensureCompletedTransitionsByProperty().set(property, element.ensureRunningTransitionsByProperty().take(property));
animation = nullptr;
}
auto beforeChangeStyle = [&]() -> const RenderStyle {
if (animation && animation->isRelevant()) {
auto animatedStyle = RenderStyle::clone(currentStyle);
bool shouldUseTimelineTimeAtCreation = is<CSSTransition>(animation) && (!animation->startTime() || *animation->startTime() == currentTime());
animation->resolve(animatedStyle, shouldUseTimelineTimeAtCreation ? downcast<CSSTransition>(*animation).timelineTimeAtCreation() : WTF::nullopt);
return animatedStyle;
}
if (auto* lastStyleChangeEventStyle = element.lastStyleChangeEventStyle())
return RenderStyle::clone(*lastStyleChangeEventStyle);
return RenderStyle::clone(currentStyle);
}();
auto afterChangeStyle = [&]() -> const RenderStyle {
if (is<CSSAnimation>(animation) && animation->isRelevant()) {
auto animatedStyle = RenderStyle::clone(newStyle);
animation->resolve(animatedStyle);
return animatedStyle;
}
return RenderStyle::clone(newStyle);
}();
if (!element.hasRunningTransitionsForProperty(property)
&& !CSSPropertyAnimation::propertiesEqual(property, &beforeChangeStyle, &afterChangeStyle)
&& CSSPropertyAnimation::canPropertyBeInterpolated(property, &beforeChangeStyle, &afterChangeStyle)
&& !propertyInStyleMatchesValueForTransitionInMap(property, afterChangeStyle, element.ensureCompletedTransitionsByProperty())
&& matchingBackingAnimation && transitionCombinedDuration(matchingBackingAnimation) > 0) {
element.ensureCompletedTransitionsByProperty().remove(property);
auto delay = Seconds(matchingBackingAnimation->delay());
auto duration = Seconds(matchingBackingAnimation->duration());
auto& reversingAdjustedStartStyle = beforeChangeStyle;
auto reversingShorteningFactor = 1;
element.ensureRunningTransitionsByProperty().set(property, CSSTransition::create(element, property, generationTime, *matchingBackingAnimation, &beforeChangeStyle, afterChangeStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor));
} else if (element.hasCompletedTransitionsForProperty(property) && !propertyInStyleMatchesValueForTransitionInMap(property, afterChangeStyle, element.ensureCompletedTransitionsByProperty())) {
element.ensureCompletedTransitionsByProperty().remove(property);
}
bool hasRunningTransition = element.hasRunningTransitionsForProperty(property);
if ((hasRunningTransition || element.hasCompletedTransitionsForProperty(property)) && !matchingBackingAnimation) {
if (hasRunningTransition)
element.ensureRunningTransitionsByProperty().take(property)->cancel();
else
element.ensureCompletedTransitionsByProperty().remove(property);
}
if (matchingBackingAnimation && element.hasRunningTransitionsForProperty(property) && !propertyInStyleMatchesValueForTransitionInMap(property, afterChangeStyle, element.ensureRunningTransitionsByProperty())) {
auto previouslyRunningTransition = element.ensureRunningTransitionsByProperty().take(property);
auto& previouslyRunningTransitionCurrentStyle = previouslyRunningTransition->currentStyle();
if (CSSPropertyAnimation::propertiesEqual(property, &previouslyRunningTransitionCurrentStyle, &afterChangeStyle) || !CSSPropertyAnimation::canPropertyBeInterpolated(property, ¤tStyle, &afterChangeStyle)) {
previouslyRunningTransition->cancelFromStyle();
} else if (transitionCombinedDuration(matchingBackingAnimation) <= 0.0 || !CSSPropertyAnimation::canPropertyBeInterpolated(property, &previouslyRunningTransitionCurrentStyle, &afterChangeStyle)) {
previouslyRunningTransition->cancelFromStyle();
} else if (CSSPropertyAnimation::propertiesEqual(property, &previouslyRunningTransition->reversingAdjustedStartStyle(), &afterChangeStyle)) {
previouslyRunningTransition->cancelFromStyle();
auto& reversingAdjustedStartStyle = previouslyRunningTransition->targetStyle();
double transformedProgress = 1;
if (auto* effect = previouslyRunningTransition->effect()) {
if (auto computedTimingProgress = effect->getComputedTiming().progress)
transformedProgress = *computedTimingProgress;
}
auto reversingShorteningFactor = std::max(std::min(((transformedProgress * previouslyRunningTransition->reversingShorteningFactor()) + (1 - previouslyRunningTransition->reversingShorteningFactor())), 1.0), 0.0);
auto delay = matchingBackingAnimation->delay() < 0 ? Seconds(matchingBackingAnimation->delay()) * reversingShorteningFactor : Seconds(matchingBackingAnimation->delay());
auto duration = Seconds(matchingBackingAnimation->duration()) * reversingShorteningFactor;
element.ensureRunningTransitionsByProperty().set(property, CSSTransition::create(element, property, generationTime, *matchingBackingAnimation, &previouslyRunningTransitionCurrentStyle, afterChangeStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor));
} else {
previouslyRunningTransition->cancelFromStyle();
auto delay = Seconds(matchingBackingAnimation->delay());
auto duration = Seconds(matchingBackingAnimation->duration());
auto& reversingAdjustedStartStyle = currentStyle;
auto reversingShorteningFactor = 1;
element.ensureRunningTransitionsByProperty().set(property, CSSTransition::create(element, property, generationTime, *matchingBackingAnimation, &previouslyRunningTransitionCurrentStyle, afterChangeStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor));
}
}
}
void AnimationTimeline::updateCSSTransitionsForElement(Element& element, const RenderStyle& currentStyle, const RenderStyle& newStyle)
{
if (currentStyle.hasTransitions() && currentStyle.display() != DisplayType::None && newStyle.display() == DisplayType::None) {
if (element.hasRunningTransitions()) {
auto runningTransitions = element.ensureRunningTransitionsByProperty();
for (const auto& cssTransitionsByCSSPropertyIDMapItem : runningTransitions)
cssTransitionsByCSSPropertyIDMapItem.value->cancelFromStyle();
}
return;
}
auto generationTime = MonotonicTime::now();
bool transitionPropertiesContainAll = false;
HashSet<CSSPropertyID> transitionProperties;
compileTransitionPropertiesInStyle(currentStyle, transitionProperties, transitionPropertiesContainAll);
compileTransitionPropertiesInStyle(newStyle, transitionProperties, transitionPropertiesContainAll);
if (transitionPropertiesContainAll) {
auto numberOfProperties = CSSPropertyAnimation::getNumProperties();
for (int propertyIndex = 0; propertyIndex < numberOfProperties; ++propertyIndex) {
Optional<bool> isShorthand;
auto property = CSSPropertyAnimation::getPropertyAtIndex(propertyIndex, isShorthand);
if (isShorthand && *isShorthand)
continue;
updateCSSTransitionsForElementAndProperty(element, property, currentStyle, newStyle, generationTime);
}
return;
}
for (auto property : transitionProperties)
updateCSSTransitionsForElementAndProperty(element, property, currentStyle, newStyle, generationTime);
}
}