7ed02ce2b677b0000665a34754f52b09d2ef9ad0
[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 }
55
56 void AnimationTimeline::addAnimation(Ref<WebAnimation>&& animation)
57 {
58     m_animations.add(WTFMove(animation));
59     timingModelDidChange();
60 }
61
62 void AnimationTimeline::removeAnimation(Ref<WebAnimation>&& animation)
63 {
64     m_animations.remove(WTFMove(animation));
65     timingModelDidChange();
66 }
67
68 std::optional<double> AnimationTimeline::bindingsCurrentTime()
69 {
70     auto time = currentTime();
71     if (!time)
72         return std::nullopt;
73     return secondsToWebAnimationsAPITime(*time);
74 }
75
76 void AnimationTimeline::setCurrentTime(Seconds currentTime)
77 {
78     m_currentTime = currentTime;
79     timingModelDidChange();
80 }
81
82 HashMap<Element*, ListHashSet<RefPtr<WebAnimation>>>& AnimationTimeline::relevantMapForAnimation(WebAnimation& animation)
83 {
84     if (animation.isCSSAnimation())
85         return m_elementToCSSAnimationsMap;
86     if (animation.isCSSTransition())
87         return m_elementToCSSTransitionsMap;
88     return m_elementToAnimationsMap;
89 }
90
91 void AnimationTimeline::animationWasAddedToElement(WebAnimation& animation, Element& element)
92 {
93     relevantMapForAnimation(animation).ensure(&element, [] {
94         return ListHashSet<RefPtr<WebAnimation>> { };
95     }).iterator->value.add(&animation);
96 }
97
98 void AnimationTimeline::animationWasRemovedFromElement(WebAnimation& animation, Element& element)
99 {
100     // First, we clear this animation from one of the m_elementToCSSAnimationsMap, m_elementToCSSTransitionsMap
101     // or m_elementToAnimationsMap map, whichever is relevant to this type of animation.
102     auto& map = relevantMapForAnimation(animation);
103     auto iterator = map.find(&element);
104     if (iterator == map.end())
105         return;
106
107     auto& animations = iterator->value;
108     animations.remove(&animation);
109     if (!animations.size())
110         map.remove(iterator);
111
112     // Now, if we're dealing with a declarative animation, we remove it from either the m_elementToCSSAnimationByName
113     // or the m_elementToCSSTransitionByCSSPropertyID map, whichever is relevant to this type of animation.
114     if (is<CSSAnimation>(animation)) {
115         auto iterator = m_elementToCSSAnimationByName.find(&element);
116         if (iterator != m_elementToCSSAnimationByName.end()) {
117             auto& cssAnimationsByName = iterator->value;
118             auto& name = downcast<CSSAnimation>(animation).animationName();
119             cssAnimationsByName.remove(name);
120             if (cssAnimationsByName.isEmpty())
121                 m_elementToCSSAnimationByName.remove(&element);
122         }
123     } else if (is<CSSTransition>(animation)) {
124         auto iterator = m_elementToCSSTransitionByCSSPropertyID.find(&element);
125         if (iterator != m_elementToCSSTransitionByCSSPropertyID.end()) {
126             auto& cssTransitionsByProperty = iterator->value;
127             auto property = downcast<CSSTransition>(animation).property();
128             cssTransitionsByProperty.remove(property);
129             if (cssTransitionsByProperty.isEmpty())
130                 m_elementToCSSTransitionByCSSPropertyID.remove(&element);
131         }
132     }
133 }
134
135 Vector<RefPtr<WebAnimation>> AnimationTimeline::animationsForElement(Element& element) const
136 {
137     Vector<RefPtr<WebAnimation>> animations;
138     if (m_elementToCSSTransitionsMap.contains(&element)) {
139         const auto& cssTransitions = m_elementToCSSTransitionsMap.get(&element);
140         animations.appendRange(cssTransitions.begin(), cssTransitions.end());
141     }
142     if (m_elementToCSSAnimationsMap.contains(&element)) {
143         const auto& cssAnimations = m_elementToCSSAnimationsMap.get(&element);
144         animations.appendRange(cssAnimations.begin(), cssAnimations.end());
145     }
146     if (m_elementToAnimationsMap.contains(&element)) {
147         const auto& webAnimations = m_elementToAnimationsMap.get(&element);
148         animations.appendRange(webAnimations.begin(), webAnimations.end());
149     }
150     return animations;
151 }
152
153 void AnimationTimeline::removeAnimationsForElement(Element& element)
154 {
155     for (auto& animation : animationsForElement(element)) {
156         animation->prepareAnimationForRemoval();
157         removeAnimation(animation.releaseNonNull());
158     }
159 }
160
161 void AnimationTimeline::updateCSSAnimationsForElement(Element& element, const RenderStyle& newStyle, const RenderStyle* oldStyle)
162 {
163     if (element.document().pageCacheState() != Document::NotInPageCache)
164         return;
165
166     if (element.document().renderView()->printing())
167         return;
168
169     // In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
170     if (oldStyle && oldStyle->hasAnimations() && oldStyle->display() != DisplayType::None && newStyle.display() == DisplayType::None) {
171         if (m_elementToCSSAnimationByName.contains(&element)) {
172             for (const auto& cssAnimationsByNameMapItem : m_elementToCSSAnimationByName.take(&element))
173                 cancelOrRemoveDeclarativeAnimation(cssAnimationsByNameMapItem.value);
174         }
175         return;
176     }
177
178     if (oldStyle && oldStyle->hasAnimations() && newStyle.hasAnimations() && *(oldStyle->animations()) == *(newStyle.animations()))
179         return;
180
181     // First, compile the list of animation names that were applied to this element up to this point.
182     HashSet<String> namesOfPreviousAnimations;
183     if (oldStyle && oldStyle->hasAnimations()) {
184         auto* previousAnimations = oldStyle->animations();
185         for (size_t i = 0; i < previousAnimations->size(); ++i) {
186             auto& previousAnimation = previousAnimations->animation(i);
187             if (previousAnimation.isValidAnimation())
188                 namesOfPreviousAnimations.add(previousAnimation.name());
189         }
190     }
191
192     // Create or get the CSSAnimations by animation name map for this element.
193     auto& cssAnimationsByName = m_elementToCSSAnimationByName.ensure(&element, [] {
194         return HashMap<String, RefPtr<CSSAnimation>> { };
195     }).iterator->value;
196
197     if (auto* currentAnimations = newStyle.animations()) {
198         for (size_t i = 0; i < currentAnimations->size(); ++i) {
199             auto& currentAnimation = currentAnimations->animation(i);
200             auto& name = currentAnimation.name();
201             if (namesOfPreviousAnimations.contains(name)) {
202                 // We've found the name of this animation in our list of previous animations, this means we've already
203                 // created a CSSAnimation object for it and need to ensure that this CSSAnimation is backed by the current
204                 // animation object for this animation name.
205                 cssAnimationsByName.get(name)->setBackingAnimation(currentAnimation);
206             } else if (currentAnimation.isValidAnimation()) {
207                 // Otherwise we are dealing with a new animation name and must create a CSSAnimation for it.
208                 cssAnimationsByName.set(name, CSSAnimation::create(element, currentAnimation, oldStyle, newStyle));
209             }
210             // Remove the name of this animation from our list since it's now known to be current.
211             namesOfPreviousAnimations.remove(name);
212         }
213     }
214
215     // The animations names left in namesOfPreviousAnimations are now known to no longer apply so we need to
216     // remove the CSSAnimation object created for them.
217     for (const auto& nameOfAnimationToRemove : namesOfPreviousAnimations)
218         cancelOrRemoveDeclarativeAnimation(cssAnimationsByName.take(nameOfAnimationToRemove));
219
220     // Remove the map of CSSAnimations by animation name for this element if it's now empty.
221     if (cssAnimationsByName.isEmpty())
222         m_elementToCSSAnimationByName.remove(&element);
223 }
224
225 RefPtr<WebAnimation> AnimationTimeline::cssAnimationForElementAndProperty(Element& element, CSSPropertyID property)
226 {
227     RefPtr<WebAnimation> matchingAnimation;
228     for (const auto& animation : m_elementToCSSAnimationsMap.get(&element)) {
229         auto* effect = animation->effect();
230         if (is<KeyframeEffectReadOnly>(effect) && downcast<KeyframeEffectReadOnly>(effect)->animatedProperties().contains(property))
231             matchingAnimation = animation;
232     }
233     return matchingAnimation;
234 }
235
236 static bool shouldBackingAnimationBeConsideredForCSSTransition(const Animation& backingAnimation)
237 {
238     auto mode = backingAnimation.animationMode();
239     if (mode == Animation::AnimateNone || mode == Animation::AnimateUnknownProperty)
240         return false;
241     if (mode == Animation::AnimateSingleProperty && backingAnimation.property() == CSSPropertyInvalid)
242         return false;
243     return true;
244 }
245
246 void AnimationTimeline::updateCSSTransitionsForElement(Element& element, const RenderStyle& newStyle, const RenderStyle* oldStyle)
247 {
248     if (element.document().pageCacheState() != Document::NotInPageCache)
249         return;
250
251     if (element.document().renderView()->printing())
252         return;
253
254     // In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
255     if (oldStyle && oldStyle->hasTransitions() && oldStyle->display() != DisplayType::None && newStyle.display() == DisplayType::None) {
256         if (m_elementToCSSTransitionByCSSPropertyID.contains(&element)) {
257             for (const auto& cssTransitionsByCSSPropertyIDMapItem : m_elementToCSSTransitionByCSSPropertyID.take(&element))
258                 cancelOrRemoveDeclarativeAnimation(cssTransitionsByCSSPropertyIDMapItem.value);
259         }
260         return;
261     }
262
263     // Create or get the CSSTransitions by CSS property name map for this element.
264     auto& cssTransitionsByProperty = m_elementToCSSTransitionByCSSPropertyID.ensure(&element, [] {
265         return HashMap<CSSPropertyID, RefPtr<CSSTransition>> { };
266     }).iterator->value;
267
268     // First, compile the list of backing animations and properties that were applied to this element up to this point.
269     auto previousProperties = copyToVector(cssTransitionsByProperty.keys());
270     HashSet<const Animation*> previousBackingAnimations;
271     if (oldStyle && oldStyle->hasTransitions()) {
272         auto* previousTransitions = oldStyle->transitions();
273         for (size_t i = 0; i < previousTransitions->size(); ++i) {
274             auto& backingAnimation = previousTransitions->animation(i);
275             if (shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
276                 previousBackingAnimations.add(&backingAnimation);
277         }
278     }
279
280     if (auto* currentTransitions = newStyle.transitions()) {
281         for (size_t i = 0; i < currentTransitions->size(); ++i) {
282             auto& backingAnimation = currentTransitions->animation(i);
283             if (!shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
284                 continue;
285             auto property = backingAnimation.property();
286             bool transitionsAllProperties = backingAnimation.animationMode() == Animation::AnimateAll;
287             auto numberOfProperties = CSSPropertyAnimation::getNumProperties();
288             // In the "transition-property: all" case, where the animation's mode is set to AnimateAll,
289             // the property will be set to CSSPropertyInvalid and we need to iterate over all known
290             // CSS properties and see if they have mis-matching values in the old and new styles, which
291             // means they should have a CSSTransition created for them.
292             // We implement a single loop which handles the "all" case and the specified property case
293             // by using the pre-set property above in the specified property case and breaking out of
294             // the loop after the first complete iteration.
295             for (int propertyIndex = 0; propertyIndex < numberOfProperties; ++propertyIndex) {
296                 if (transitionsAllProperties) {
297                     bool isShorthand;
298                     property = CSSPropertyAnimation::getPropertyAtIndex(propertyIndex, isShorthand);
299                     if (isShorthand)
300                         continue;
301                 } else if (propertyIndex) {
302                     // We only go once through this loop if we are transitioning a single property.
303                     break;
304                 }
305
306                 previousProperties.removeFirst(property);
307                 // We've found a backing animation that we didn't know about for a valid property.
308                 if (!previousBackingAnimations.contains(&backingAnimation)) {
309                     // If we already had a CSSTransition for this property, check whether its timing properties match the current backing
310                     // animation's properties and whether its blending keyframes match the old and new styles. If they do, move on to the
311                     // next transition, otherwise delete the previous CSSTransition object, and create a new one.
312                     if (cssTransitionsByProperty.contains(property)) {
313                         if (cssTransitionsByProperty.get(property)->matchesBackingAnimationAndStyles(backingAnimation, oldStyle, newStyle))
314                             continue;
315                         removeAnimation(cssTransitionsByProperty.take(property).releaseNonNull());
316                     }
317                     // Now we can create a new CSSTransition with the new backing animation provided it has a valid
318                     // duration and the from and to values are distinct.
319                     if ((backingAnimation.duration() || backingAnimation.delay() > 0) && oldStyle) {
320                         auto existingAnimation = cssAnimationForElementAndProperty(element, property);
321                         const auto* fromStyle = existingAnimation ? &downcast<CSSAnimation>(existingAnimation.get())->unanimatedStyle() : oldStyle;
322                         if (!CSSPropertyAnimation::propertiesEqual(property, fromStyle, &newStyle))
323                             cssTransitionsByProperty.set(property, CSSTransition::create(element, property, backingAnimation, fromStyle, newStyle));
324                     }
325                 }
326             }
327         }
328     }
329
330     // Remaining properties are no longer current and must be removed.
331     for (const auto transitionPropertyToRemove : previousProperties) {
332         if (cssTransitionsByProperty.contains(transitionPropertyToRemove))
333             cancelOrRemoveDeclarativeAnimation(cssTransitionsByProperty.take(transitionPropertyToRemove));
334     }
335
336     // Remove the map of CSSTransitions by property for this element if it's now empty.
337     if (cssTransitionsByProperty.isEmpty())
338         m_elementToCSSTransitionByCSSPropertyID.remove(&element);
339 }
340
341 void AnimationTimeline::cancelOrRemoveDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
342 {
343     auto phase = animation->effect()->phase();
344     if (phase != AnimationEffectReadOnly::Phase::Idle && phase != AnimationEffectReadOnly::Phase::After)
345         animation->cancel();
346     else
347         removeAnimation(animation.releaseNonNull());
348 }
349
350 String AnimationTimeline::description()
351 {
352     TextStream stream;
353     int count = 1;
354     stream << (m_classType == DocumentTimelineClass ? "DocumentTimeline" : "AnimationTimeline") << " with " << m_animations.size() << " animations:";
355     stream << "\n";
356     for (const auto& animation : m_animations) {
357         writeIndent(stream, 1);
358         stream << count << ". " << animation->description();
359         stream << "\n";
360         count++;
361     }
362     return stream.release();
363 }
364
365 } // namespace WebCore