[Web Animations] Fix a host of small CSS Animations and CSS Transitions issues
[WebKit-https.git] / Source / WebCore / animation / AnimationTimeline.cpp
1 /*
2  * Copyright (C) Canon Inc. 2016
3  * Copyright (C) 2017 Apple Inc. All rights reserved.
4  *
5  * Redistribution and use in source and binary forms, with or without
6  * modification, are permitted provided that the following conditions
7  * are met:
8  * 1. Redistributions of source code must retain the above copyright
9  *    notice, this list of conditions and the following disclaimer.
10  * 2. Redistributions in binary form must reproduce the above copyright
11  *    notice, this list of conditions and the following disclaimer in the
12  *    documentation and/or other materials provided with the distribution.
13  *
14  * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
15  * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17  * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
18  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22  * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23  * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24  * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25  */
26
27 #include "config.h"
28 #include "AnimationTimeline.h"
29
30 #include "Animation.h"
31 #include "AnimationEffectReadOnly.h"
32 #include "AnimationList.h"
33 #include "CSSAnimation.h"
34 #include "CSSPropertyAnimation.h"
35 #include "CSSTransition.h"
36 #include "DocumentTimeline.h"
37 #include "Element.h"
38 #include "KeyframeEffectReadOnly.h"
39 #include "RenderStyle.h"
40 #include "RenderView.h"
41 #include "WebAnimationUtilities.h"
42 #include <wtf/text/TextStream.h>
43 #include <wtf/text/WTFString.h>
44
45 namespace WebCore {
46
47 AnimationTimeline::AnimationTimeline(ClassType classType)
48     : m_classType(classType)
49 {
50 }
51
52 AnimationTimeline::~AnimationTimeline()
53 {
54     m_animations.clear();
55     m_elementToAnimationsMap.clear();
56     m_elementToCSSAnimationsMap.clear();
57     m_elementToCSSTransitionsMap.clear();
58     m_elementToCSSAnimationByName.clear();
59     m_elementToCSSTransitionByCSSPropertyID.clear();
60 }
61
62 void AnimationTimeline::addAnimation(Ref<WebAnimation>&& animation)
63 {
64     m_animations.add(WTFMove(animation));
65     timingModelDidChange();
66 }
67
68 void AnimationTimeline::removeAnimation(Ref<WebAnimation>&& animation)
69 {
70     m_animations.remove(WTFMove(animation));
71     timingModelDidChange();
72 }
73
74 std::optional<double> AnimationTimeline::bindingsCurrentTime()
75 {
76     auto time = currentTime();
77     if (!time)
78         return std::nullopt;
79     return secondsToWebAnimationsAPITime(*time);
80 }
81
82 void AnimationTimeline::setCurrentTime(Seconds currentTime)
83 {
84     m_currentTime = currentTime;
85     timingModelDidChange();
86 }
87
88 HashMap<Element*, Vector<RefPtr<WebAnimation>>>& AnimationTimeline::relevantMapForAnimation(WebAnimation& animation)
89 {
90     if (animation.isCSSAnimation())
91         return m_elementToCSSAnimationsMap;
92     if (animation.isCSSTransition())
93         return m_elementToCSSTransitionsMap;
94     return m_elementToAnimationsMap;
95 }
96
97 void AnimationTimeline::animationWasAddedToElement(WebAnimation& animation, Element& element)
98 {
99     auto result = relevantMapForAnimation(animation).ensure(&element, [] {
100         return Vector<RefPtr<WebAnimation>> { };
101     });
102     result.iterator->value.append(&animation);
103 }
104
105 void AnimationTimeline::animationWasRemovedFromElement(WebAnimation& animation, Element& element)
106 {
107     auto& map = relevantMapForAnimation(animation);
108     auto iterator = map.find(&element);
109     if (iterator == map.end())
110         return;
111
112     auto& animations = iterator->value;
113     animations.removeFirst(&animation);
114     if (!animations.size())
115         map.remove(iterator);
116 }
117
118 Vector<RefPtr<WebAnimation>> AnimationTimeline::animationsForElement(Element& element) const
119 {
120     Vector<RefPtr<WebAnimation>> animations;
121     if (m_elementToCSSAnimationsMap.contains(&element))
122         animations.appendVector(m_elementToCSSAnimationsMap.get(&element));
123     if (m_elementToCSSTransitionsMap.contains(&element))
124         animations.appendVector(m_elementToCSSTransitionsMap.get(&element));
125     if (m_elementToAnimationsMap.contains(&element))
126         animations.appendVector(m_elementToAnimationsMap.get(&element));
127     return animations;
128 }
129
130 void AnimationTimeline::cancelDeclarativeAnimationsForElement(Element& element)
131 {
132     for (const auto& animation : animationsForElement(element)) {
133         if (is<DeclarativeAnimation>(animation))
134             animation->cancel();
135     }
136 }
137
138 void AnimationTimeline::updateCSSAnimationsForElement(Element& element, const RenderStyle& newStyle, const RenderStyle* oldStyle)
139 {
140     if (element.document().pageCacheState() != Document::NotInPageCache)
141         return;
142
143     if (element.document().renderView()->printing())
144         return;
145
146     // In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
147     if (oldStyle && oldStyle->hasAnimations() && oldStyle->display() != NONE && newStyle.display() == NONE) {
148         if (m_elementToCSSAnimationByName.contains(&element)) {
149             for (const auto& cssAnimationsByNameMapItem : m_elementToCSSAnimationByName.take(&element))
150                 cancelOrRemoveDeclarativeAnimation(cssAnimationsByNameMapItem.value);
151         }
152         return;
153     }
154
155     if (oldStyle && oldStyle->hasAnimations() && newStyle.hasAnimations() && *(oldStyle->animations()) == *(newStyle.animations()))
156         return;
157
158     // First, compile the list of animation names that were applied to this element up to this point.
159     HashSet<String> namesOfPreviousAnimations;
160     if (oldStyle && oldStyle->hasAnimations()) {
161         auto* previousAnimations = oldStyle->animations();
162         for (size_t i = 0; i < previousAnimations->size(); ++i) {
163             auto& previousAnimation = previousAnimations->animation(i);
164             if (previousAnimation.isValidAnimation())
165                 namesOfPreviousAnimations.add(previousAnimation.name());
166         }
167     }
168
169     // Create or get the CSSAnimations by animation name map for this element.
170     auto& cssAnimationsByName = m_elementToCSSAnimationByName.ensure(&element, [] {
171         return HashMap<String, RefPtr<CSSAnimation>> { };
172     }).iterator->value;
173
174     if (auto* currentAnimations = newStyle.animations()) {
175         for (size_t i = 0; i < currentAnimations->size(); ++i) {
176             auto& currentAnimation = currentAnimations->animation(i);
177             auto& name = currentAnimation.name();
178             if (namesOfPreviousAnimations.contains(name)) {
179                 // We've found the name of this animation in our list of previous animations, this means we've already
180                 // created a CSSAnimation object for it and need to ensure that this CSSAnimation is backed by the current
181                 // animation object for this animation name.
182                 cssAnimationsByName.get(name)->setBackingAnimation(currentAnimation);
183             } else if (currentAnimation.isValidAnimation()) {
184                 // Otherwise we are dealing with a new animation name and must create a CSSAnimation for it.
185                 cssAnimationsByName.set(name, CSSAnimation::create(element, currentAnimation, oldStyle, newStyle));
186             }
187             // Remove the name of this animation from our list since it's now known to be current.
188             namesOfPreviousAnimations.remove(name);
189         }
190     }
191
192     // The animations names left in namesOfPreviousAnimations are now known to no longer apply so we need to
193     // remove the CSSAnimation object created for them.
194     for (const auto& nameOfAnimationToRemove : namesOfPreviousAnimations)
195         cancelOrRemoveDeclarativeAnimation(cssAnimationsByName.take(nameOfAnimationToRemove));
196
197     // Remove the map of CSSAnimations by animation name for this element if it's now empty.
198     if (cssAnimationsByName.isEmpty())
199         m_elementToCSSAnimationByName.remove(&element);
200 }
201
202 RefPtr<WebAnimation> AnimationTimeline::cssAnimationForElementAndProperty(Element& element, CSSPropertyID property)
203 {
204     RefPtr<WebAnimation> matchingAnimation;
205     for (const auto& animation : m_elementToCSSAnimationsMap.get(&element)) {
206         auto* effect = animation->effect();
207         if (is<KeyframeEffectReadOnly>(effect) && downcast<KeyframeEffectReadOnly>(effect)->animatedProperties().contains(property))
208             matchingAnimation = animation;
209     }
210     return matchingAnimation;
211 }
212
213 static bool shouldBackingAnimationBeConsideredForCSSTransition(const Animation& backingAnimation)
214 {
215     auto mode = backingAnimation.animationMode();
216     if (mode == Animation::AnimateNone || mode == Animation::AnimateUnknownProperty)
217         return false;
218     if (mode == Animation::AnimateSingleProperty && backingAnimation.property() == CSSPropertyInvalid)
219         return false;
220     return true;
221 }
222
223 void AnimationTimeline::updateCSSTransitionsForElement(Element& element, const RenderStyle& newStyle, const RenderStyle* oldStyle)
224 {
225     if (element.document().pageCacheState() != Document::NotInPageCache)
226         return;
227
228     if (element.document().renderView()->printing())
229         return;
230
231     // In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
232     if (oldStyle && oldStyle->hasTransitions() && oldStyle->display() != NONE && newStyle.display() == NONE) {
233         if (m_elementToCSSTransitionByCSSPropertyID.contains(&element)) {
234             for (const auto& cssTransitionsByCSSPropertyIDMapItem : m_elementToCSSTransitionByCSSPropertyID.take(&element))
235                 cancelOrRemoveDeclarativeAnimation(cssTransitionsByCSSPropertyIDMapItem.value);
236         }
237         return;
238     }
239
240     // Create or get the CSSTransitions by CSS property name map for this element.
241     auto& cssTransitionsByProperty = m_elementToCSSTransitionByCSSPropertyID.ensure(&element, [] {
242         return HashMap<CSSPropertyID, RefPtr<CSSTransition>> { };
243     }).iterator->value;
244
245     // First, compile the list of backing animations and properties that were applied to this element up to this point.
246     auto previousProperties = copyToVector(cssTransitionsByProperty.keys());
247     HashSet<const Animation*> previousBackingAnimations;
248     if (oldStyle && oldStyle->hasTransitions()) {
249         auto* previousTransitions = oldStyle->transitions();
250         for (size_t i = 0; i < previousTransitions->size(); ++i) {
251             auto& backingAnimation = previousTransitions->animation(i);
252             if (shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
253                 previousBackingAnimations.add(&backingAnimation);
254         }
255     }
256
257     if (auto* currentTransitions = newStyle.transitions()) {
258         for (size_t i = 0; i < currentTransitions->size(); ++i) {
259             auto& backingAnimation = currentTransitions->animation(i);
260             if (!shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
261                 continue;
262             auto property = backingAnimation.property();
263             bool transitionsAllProperties = backingAnimation.animationMode() == Animation::AnimateAll;
264             auto numberOfProperties = CSSPropertyAnimation::getNumProperties();
265             // In the "transition-property: all" case, where the animation's mode is set to AnimateAll,
266             // the property will be set to CSSPropertyInvalid and we need to iterate over all known
267             // CSS properties and see if they have mis-matching values in the old and new styles, which
268             // means they should have a CSSTransition created for them.
269             // We implement a single loop which handles the "all" case and the specified property case
270             // by using the pre-set property above in the specified property case and breaking out of
271             // the loop after the first complete iteration.
272             for (int propertyIndex = 0; propertyIndex < numberOfProperties; ++propertyIndex) {
273                 if (transitionsAllProperties) {
274                     bool isShorthand;
275                     property = CSSPropertyAnimation::getPropertyAtIndex(propertyIndex, isShorthand);
276                     if (isShorthand)
277                         continue;
278                 } else if (propertyIndex) {
279                     // We only go once through this loop if we are transitioning a single property.
280                     break;
281                 }
282
283                 previousProperties.removeFirst(property);
284                 // We've found a backing animation that we didn't know about for a valid property.
285                 if (!previousBackingAnimations.contains(&backingAnimation)) {
286                     // If we already had a CSSTransition for this property, check whether its timing properties match the current backing
287                     // animation's properties and whether its blending keyframes match the old and new styles. If they do, move on to the
288                     // next transition, otherwise delete the previous CSSTransition object, and create a new one.
289                     if (cssTransitionsByProperty.contains(property)) {
290                         if (cssTransitionsByProperty.get(property)->matchesBackingAnimationAndStyles(backingAnimation, oldStyle, newStyle))
291                             continue;
292                         removeDeclarativeAnimation(cssTransitionsByProperty.take(property));
293                     }
294                     // Now we can create a new CSSTransition with the new backing animation provided it has a valid
295                     // duration and the from and to values are distinct.
296                     if ((backingAnimation.duration() || backingAnimation.delay() > 0) && oldStyle) {
297                         auto existingAnimation = cssAnimationForElementAndProperty(element, property);
298                         const auto* fromStyle = existingAnimation ? &downcast<CSSAnimation>(existingAnimation.get())->unanimatedStyle() : oldStyle;
299                         if (!CSSPropertyAnimation::propertiesEqual(property, fromStyle, &newStyle))
300                             cssTransitionsByProperty.set(property, CSSTransition::create(element, property, backingAnimation, fromStyle, newStyle));
301                     }
302                 }
303             }
304         }
305     }
306
307     // Remaining properties are no longer current and must be removed.
308     for (const auto transitionPropertyToRemove : previousProperties) {
309         if (cssTransitionsByProperty.contains(transitionPropertyToRemove))
310             cancelOrRemoveDeclarativeAnimation(cssTransitionsByProperty.take(transitionPropertyToRemove));
311     }
312
313     // Remove the map of CSSTransitions by property for this element if it's now empty.
314     if (cssTransitionsByProperty.isEmpty())
315         m_elementToCSSTransitionByCSSPropertyID.remove(&element);
316 }
317
318 void AnimationTimeline::removeDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
319 {
320     animation->setEffect(nullptr);
321     removeAnimation(animation.releaseNonNull());
322 }
323
324 void AnimationTimeline::cancelOrRemoveDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
325 {
326     auto phase = animation->effect()->phase();
327     if (phase != AnimationEffectReadOnly::Phase::Idle && phase != AnimationEffectReadOnly::Phase::After)
328         animation->cancel();
329     else
330         removeDeclarativeAnimation(animation);
331 }
332
333 String AnimationTimeline::description()
334 {
335     TextStream stream;
336     int count = 1;
337     stream << (m_classType == DocumentTimelineClass ? "DocumentTimeline" : "AnimationTimeline") << " with " << m_animations.size() << " animations:";
338     stream << "\n";
339     for (const auto& animation : m_animations) {
340         writeIndent(stream, 1);
341         stream << count << ". " << animation->description();
342         stream << "\n";
343         count++;
344     }
345     return stream.release();
346 }
347
348 } // namespace WebCore