[Web Animations] Support "transition: all" for CSS Transitions as Web Animations
[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)
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::updateCSSAnimationsForElement(Element& element, const RenderStyle& newStyle, const RenderStyle* oldStyle)
131 {
132     if (element.document().pageCacheState() != Document::NotInPageCache)
133         return;
134
135     if (element.document().renderView()->printing())
136         return;
137
138     // In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
139     if (oldStyle && oldStyle->hasAnimations() && oldStyle->display() != NONE && newStyle.display() == NONE) {
140         if (m_elementToCSSAnimationByName.contains(&element)) {
141             for (const auto& cssAnimationsByNameMapItem : m_elementToCSSAnimationByName.take(&element))
142                 cancelOrRemoveDeclarativeAnimation(cssAnimationsByNameMapItem.value);
143         }
144         return;
145     }
146
147     if (oldStyle && oldStyle->hasAnimations() && newStyle.hasAnimations() && *(oldStyle->animations()) == *(newStyle.animations()))
148         return;
149
150     // First, compile the list of animation names that were applied to this element up to this point.
151     HashSet<String> namesOfPreviousAnimations;
152     if (oldStyle && oldStyle->hasAnimations()) {
153         auto* previousAnimations = oldStyle->animations();
154         for (size_t i = 0; i < previousAnimations->size(); ++i) {
155             auto& previousAnimation = previousAnimations->animation(i);
156             if (previousAnimation.isValidAnimation())
157                 namesOfPreviousAnimations.add(previousAnimation.name());
158         }
159     }
160
161     // Create or get the CSSAnimations by animation name map for this element.
162     auto& cssAnimationsByName = m_elementToCSSAnimationByName.ensure(&element, [] {
163         return HashMap<String, RefPtr<CSSAnimation>> { };
164     }).iterator->value;
165
166     if (auto* currentAnimations = newStyle.animations()) {
167         for (size_t i = 0; i < currentAnimations->size(); ++i) {
168             auto& currentAnimation = currentAnimations->animation(i);
169             auto& name = currentAnimation.name();
170             if (namesOfPreviousAnimations.contains(name)) {
171                 // We've found the name of this animation in our list of previous animations, this means we've already
172                 // created a CSSAnimation object for it and need to ensure that this CSSAnimation is backed by the current
173                 // animation object for this animation name.
174                 cssAnimationsByName.get(name)->setBackingAnimation(currentAnimation);
175             } else if (currentAnimation.isValidAnimation()) {
176                 // Otherwise we are dealing with a new animation name and must create a CSSAnimation for it.
177                 cssAnimationsByName.set(name, CSSAnimation::create(element, currentAnimation));
178             }
179             // Remove the name of this animation from our list since it's now known to be current.
180             namesOfPreviousAnimations.remove(name);
181         }
182     }
183
184     // The animations names left in namesOfPreviousAnimations are now known to no longer apply so we need to
185     // remove the CSSAnimation object created for them.
186     for (const auto& nameOfAnimationToRemove : namesOfPreviousAnimations)
187         cancelOrRemoveDeclarativeAnimation(cssAnimationsByName.take(nameOfAnimationToRemove));
188
189     // Remove the map of CSSAnimations by animation name for this element if it's now empty.
190     if (cssAnimationsByName.isEmpty())
191         m_elementToCSSAnimationByName.remove(&element);
192 }
193
194 static bool shouldBackingAnimationBeConsideredForCSSTransition(const Animation& backingAnimation)
195 {
196     auto mode = backingAnimation.animationMode();
197     if (mode == Animation::AnimateNone || mode == Animation::AnimateUnknownProperty)
198         return false;
199     if (mode == Animation::AnimateSingleProperty && backingAnimation.property() == CSSPropertyInvalid)
200         return false;
201     return true;
202 }
203
204 void AnimationTimeline::updateCSSTransitionsForElement(Element& element, const RenderStyle& newStyle, const RenderStyle* oldStyle)
205 {
206     if (element.document().pageCacheState() != Document::NotInPageCache)
207         return;
208
209     if (element.document().renderView()->printing())
210         return;
211
212     // In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
213     if (oldStyle && oldStyle->hasTransitions() && oldStyle->display() != NONE && newStyle.display() == NONE) {
214         if (m_elementToCSSTransitionByCSSPropertyID.contains(&element)) {
215             for (const auto& cssTransitionsByCSSPropertyIDMapItem : m_elementToCSSTransitionByCSSPropertyID.take(&element))
216                 cancelOrRemoveDeclarativeAnimation(cssTransitionsByCSSPropertyIDMapItem.value);
217         }
218         return;
219     }
220
221     // Create or get the CSSTransitions by CSS property name map for this element.
222     auto& cssTransitionsByProperty = m_elementToCSSTransitionByCSSPropertyID.ensure(&element, [] {
223         return HashMap<CSSPropertyID, RefPtr<CSSTransition>> { };
224     }).iterator->value;
225
226     // First, compile the list of backing animations and properties that were applied to this element up to this point.
227     auto previousProperties = copyToVector(cssTransitionsByProperty.keys());
228     HashSet<const Animation*> previousBackingAnimations;
229     if (oldStyle && oldStyle->hasTransitions()) {
230         auto* previousTransitions = oldStyle->transitions();
231         for (size_t i = 0; i < previousTransitions->size(); ++i) {
232             auto& backingAnimation = previousTransitions->animation(i);
233             if (shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
234                 previousBackingAnimations.add(&backingAnimation);
235         }
236     }
237
238     if (auto* currentTransitions = newStyle.transitions()) {
239         for (size_t i = 0; i < currentTransitions->size(); ++i) {
240             auto& backingAnimation = currentTransitions->animation(i);
241             if (!shouldBackingAnimationBeConsideredForCSSTransition(backingAnimation))
242                 continue;
243             auto property = backingAnimation.property();
244             bool transitionsAllProperties = backingAnimation.animationMode() == Animation::AnimateAll;
245             auto numberOfProperties = CSSPropertyAnimation::getNumProperties();
246             // In the "transition-property: all" case, where the animation's mode is set to AnimateAll,
247             // the property will be set to CSSPropertyInvalid and we need to iterate over all known
248             // CSS properties and see if they have mis-matching values in the old and new styles, which
249             // means they should have a CSSTransition created for them.
250             // We implement a single loop which handles the "all" case and the specified property case
251             // by using the pre-set property above in the specified property case and breaking out of
252             // the loop after the first complete iteration.
253             for (int propertyIndex = 0; propertyIndex < numberOfProperties; ++propertyIndex) {
254                 if (transitionsAllProperties) {
255                     bool isShorthand;
256                     property = CSSPropertyAnimation::getPropertyAtIndex(propertyIndex, isShorthand);
257                     if (isShorthand)
258                         continue;
259                 } else if (propertyIndex) {
260                     // We only go once through this loop if we are transitioning a single property.
261                     break;
262                 }
263
264                 bool hadProperty = previousProperties.removeFirst(property);
265                 // We've found a backing animation that we didn't know about for a valid property.
266                 if (!previousBackingAnimations.contains(&backingAnimation)) {
267                     // If we already had a CSSTransition for this property, check whether its timing properties match the current backing
268                     // animation's properties and whether its blending keyframes match the old and new styles. If they do, move on to the
269                     // next transition, otherwise delete the previous CSSTransition object, and create a new one.
270                     if (hadProperty) {
271                         if (cssTransitionsByProperty.get(property)->matchesBackingAnimationAndStyles(backingAnimation, oldStyle, newStyle))
272                             continue;
273                         removeDeclarativeAnimation(cssTransitionsByProperty.take(property));
274                     }
275                     // Now we can create a new CSSTransition with the new backing animation provided it has a valid
276                     // duration and the from and to values are distinct.
277                     if (backingAnimation.duration() > 0 && oldStyle && !CSSPropertyAnimation::propertiesEqual(property, oldStyle, &newStyle))
278                         cssTransitionsByProperty.set(property, CSSTransition::create(element, property, backingAnimation, oldStyle, newStyle));
279                 }
280             }
281         }
282     }
283
284     // Remaining properties are no longer current and must be removed.
285     for (const auto transitionPropertyToRemove : previousProperties) {
286         if (cssTransitionsByProperty.contains(transitionPropertyToRemove))
287             cancelOrRemoveDeclarativeAnimation(cssTransitionsByProperty.take(transitionPropertyToRemove));
288     }
289
290     // Remove the map of CSSTransitions by property for this element if it's now empty.
291     if (cssTransitionsByProperty.isEmpty())
292         m_elementToCSSTransitionByCSSPropertyID.remove(&element);
293 }
294
295 void AnimationTimeline::removeDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
296 {
297     animation->setEffect(nullptr);
298     removeAnimation(animation.releaseNonNull());
299 }
300
301 void AnimationTimeline::cancelOrRemoveDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
302 {
303     auto phase = animation->effect()->phase();
304     if (phase != AnimationEffectReadOnly::Phase::Idle && phase != AnimationEffectReadOnly::Phase::After)
305         animation->cancel();
306     else
307         removeDeclarativeAnimation(animation);
308 }
309
310 String AnimationTimeline::description()
311 {
312     TextStream stream;
313     int count = 1;
314     stream << (m_classType == DocumentTimelineClass ? "DocumentTimeline" : "AnimationTimeline") << " with " << m_animations.size() << " animations:";
315     stream << "\n";
316     for (const auto& animation : m_animations) {
317         writeIndent(stream, 1);
318         stream << count << ". " << animation->description();
319         stream << "\n";
320         count++;
321     }
322     return stream.release();
323 }
324
325 } // namespace WebCore