2b26ebcc93065cd59ae3c99f241e065a13f23f14
[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));
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 static bool shouldBackingAnimationBeConsideredForCSSTransition(const Animation& backingAnimation)
203 {
204     auto mode = backingAnimation.animationMode();
205     if (mode == Animation::AnimateNone || mode == Animation::AnimateUnknownProperty)
206         return false;
207     if (mode == Animation::AnimateSingleProperty && backingAnimation.property() == CSSPropertyInvalid)
208         return false;
209     return true;
210 }
211
212 void AnimationTimeline::updateCSSTransitionsForElement(Element& element, const RenderStyle& newStyle, const RenderStyle* oldStyle)
213 {
214     if (element.document().pageCacheState() != Document::NotInPageCache)
215         return;
216
217     if (element.document().renderView()->printing())
218         return;
219
220     // In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
221     if (oldStyle && oldStyle->hasTransitions() && oldStyle->display() != NONE && newStyle.display() == NONE) {
222         if (m_elementToCSSTransitionByCSSPropertyID.contains(&element)) {
223             for (const auto& cssTransitionsByCSSPropertyIDMapItem : m_elementToCSSTransitionByCSSPropertyID.take(&element))
224                 cancelOrRemoveDeclarativeAnimation(cssTransitionsByCSSPropertyIDMapItem.value);
225         }
226         return;
227     }
228
229     // Create or get the CSSTransitions by CSS property name map for this element.
230     auto& cssTransitionsByProperty = m_elementToCSSTransitionByCSSPropertyID.ensure(&element, [] {
231         return HashMap<CSSPropertyID, RefPtr<CSSTransition>> { };
232     }).iterator->value;
233
234     // First, compile the list of backing animations and properties that were applied to this element up to this point.
235     auto previousProperties = copyToVector(cssTransitionsByProperty.keys());
236     HashSet<const Animation*> previousBackingAnimations;
237     if (oldStyle && oldStyle->hasTransitions()) {
238         auto* previousTransitions = oldStyle->transitions();
239         for (size_t i = 0; i < previousTransitions->size(); ++i) {
240             auto& backingAnimation = previousTransitions->animation(i);
241             if (shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
242                 previousBackingAnimations.add(&backingAnimation);
243         }
244     }
245
246     if (auto* currentTransitions = newStyle.transitions()) {
247         for (size_t i = 0; i < currentTransitions->size(); ++i) {
248             auto& backingAnimation = currentTransitions->animation(i);
249             if (!shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
250                 continue;
251             auto property = backingAnimation.property();
252             bool transitionsAllProperties = backingAnimation.animationMode() == Animation::AnimateAll;
253             auto numberOfProperties = CSSPropertyAnimation::getNumProperties();
254             // In the "transition-property: all" case, where the animation's mode is set to AnimateAll,
255             // the property will be set to CSSPropertyInvalid and we need to iterate over all known
256             // CSS properties and see if they have mis-matching values in the old and new styles, which
257             // means they should have a CSSTransition created for them.
258             // We implement a single loop which handles the "all" case and the specified property case
259             // by using the pre-set property above in the specified property case and breaking out of
260             // the loop after the first complete iteration.
261             for (int propertyIndex = 0; propertyIndex < numberOfProperties; ++propertyIndex) {
262                 if (transitionsAllProperties) {
263                     bool isShorthand;
264                     property = CSSPropertyAnimation::getPropertyAtIndex(propertyIndex, isShorthand);
265                     if (isShorthand)
266                         continue;
267                 } else if (propertyIndex) {
268                     // We only go once through this loop if we are transitioning a single property.
269                     break;
270                 }
271
272                 bool hadProperty = previousProperties.removeFirst(property);
273                 // We've found a backing animation that we didn't know about for a valid property.
274                 if (!previousBackingAnimations.contains(&backingAnimation)) {
275                     // If we already had a CSSTransition for this property, check whether its timing properties match the current backing
276                     // animation's properties and whether its blending keyframes match the old and new styles. If they do, move on to the
277                     // next transition, otherwise delete the previous CSSTransition object, and create a new one.
278                     if (hadProperty) {
279                         if (cssTransitionsByProperty.get(property)->matchesBackingAnimationAndStyles(backingAnimation, oldStyle, newStyle))
280                             continue;
281                         removeDeclarativeAnimation(cssTransitionsByProperty.take(property));
282                     }
283                     // Now we can create a new CSSTransition with the new backing animation provided it has a valid
284                     // duration and the from and to values are distinct.
285                     if (backingAnimation.duration() > 0 && oldStyle && !CSSPropertyAnimation::propertiesEqual(property, oldStyle, &newStyle))
286                         cssTransitionsByProperty.set(property, CSSTransition::create(element, property, backingAnimation, oldStyle, newStyle));
287                 }
288             }
289         }
290     }
291
292     // Remaining properties are no longer current and must be removed.
293     for (const auto transitionPropertyToRemove : previousProperties) {
294         if (cssTransitionsByProperty.contains(transitionPropertyToRemove))
295             cancelOrRemoveDeclarativeAnimation(cssTransitionsByProperty.take(transitionPropertyToRemove));
296     }
297
298     // Remove the map of CSSTransitions by property for this element if it's now empty.
299     if (cssTransitionsByProperty.isEmpty())
300         m_elementToCSSTransitionByCSSPropertyID.remove(&element);
301 }
302
303 void AnimationTimeline::removeDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
304 {
305     animation->setEffect(nullptr);
306     removeAnimation(animation.releaseNonNull());
307 }
308
309 void AnimationTimeline::cancelOrRemoveDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
310 {
311     auto phase = animation->effect()->phase();
312     if (phase != AnimationEffectReadOnly::Phase::Idle && phase != AnimationEffectReadOnly::Phase::After)
313         animation->cancel();
314     else
315         removeDeclarativeAnimation(animation);
316 }
317
318 String AnimationTimeline::description()
319 {
320     TextStream stream;
321     int count = 1;
322     stream << (m_classType == DocumentTimelineClass ? "DocumentTimeline" : "AnimationTimeline") << " with " << m_animations.size() << " animations:";
323     stream << "\n";
324     for (const auto& animation : m_animations) {
325         writeIndent(stream, 1);
326         stream << count << ". " << animation->description();
327         stream << "\n";
328         count++;
329     }
330     return stream.release();
331 }
332
333 } // namespace WebCore