[Web Animations] Implement "Starting of transitions" section from CSS Transitions
[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 "StylePropertyShorthand.h"
42 #include "WebAnimationUtilities.h"
43 #include <wtf/text/TextStream.h>
44 #include <wtf/text/WTFString.h>
45
46 namespace WebCore {
47
48 AnimationTimeline::AnimationTimeline(ClassType classType)
49     : m_classType(classType)
50 {
51 }
52
53 AnimationTimeline::~AnimationTimeline()
54 {
55 }
56
57 void AnimationTimeline::addAnimation(Ref<WebAnimation>&& animation)
58 {
59     m_animations.add(WTFMove(animation));
60     timingModelDidChange();
61 }
62
63 void AnimationTimeline::removeAnimation(Ref<WebAnimation>&& animation)
64 {
65     m_animations.remove(WTFMove(animation));
66     timingModelDidChange();
67 }
68
69 std::optional<double> AnimationTimeline::bindingsCurrentTime()
70 {
71     auto time = currentTime();
72     if (!time)
73         return std::nullopt;
74     return secondsToWebAnimationsAPITime(*time);
75 }
76
77 void AnimationTimeline::setCurrentTime(Seconds currentTime)
78 {
79     m_currentTime = currentTime;
80     timingModelDidChange();
81 }
82
83 HashMap<Element*, ListHashSet<RefPtr<WebAnimation>>>& AnimationTimeline::relevantMapForAnimation(WebAnimation& animation)
84 {
85     if (animation.isCSSAnimation())
86         return m_elementToCSSAnimationsMap;
87     if (animation.isCSSTransition())
88         return m_elementToCSSTransitionsMap;
89     return m_elementToAnimationsMap;
90 }
91
92 void AnimationTimeline::animationWasAddedToElement(WebAnimation& animation, Element& element)
93 {
94     relevantMapForAnimation(animation).ensure(&element, [] {
95         return ListHashSet<RefPtr<WebAnimation>> { };
96     }).iterator->value.add(&animation);
97 }
98
99 static inline bool removeCSSTransitionFromMap(CSSTransition& transition, Element& element, HashMap<Element*, HashMap<CSSPropertyID, RefPtr<CSSTransition>>>& map)
100 {
101     auto iterator = map.find(&element);
102     if (iterator == map.end())
103         return false;
104
105     auto& cssTransitionsByProperty = iterator->value;
106     cssTransitionsByProperty.remove(transition.property());
107     if (cssTransitionsByProperty.isEmpty())
108         map.remove(&element);
109     return true;
110 }
111
112 void AnimationTimeline::animationWasRemovedFromElement(WebAnimation& animation, Element& element)
113 {
114     // First, we clear this animation from one of the m_elementToCSSAnimationsMap, m_elementToCSSTransitionsMap,
115     // m_elementToAnimationsMap or m_elementToCompletedCSSTransitionByCSSPropertyID map, whichever is relevant to
116     // this type of animation.
117     auto& map = relevantMapForAnimation(animation);
118     auto iterator = map.find(&element);
119     if (iterator == map.end())
120         return;
121
122     auto& animations = iterator->value;
123     animations.remove(&animation);
124     if (!animations.size())
125         map.remove(iterator);
126
127     // Now, if we're dealing with a declarative animation, we remove it from either the m_elementToCSSAnimationByName
128     // or the m_elementToRunningCSSTransitionByCSSPropertyID map, whichever is relevant to this type of animation.
129     if (is<CSSAnimation>(animation)) {
130         auto iterator = m_elementToCSSAnimationByName.find(&element);
131         if (iterator != m_elementToCSSAnimationByName.end()) {
132             auto& cssAnimationsByName = iterator->value;
133             auto& name = downcast<CSSAnimation>(animation).animationName();
134             cssAnimationsByName.remove(name);
135             if (cssAnimationsByName.isEmpty())
136                 m_elementToCSSAnimationByName.remove(&element);
137         }
138     } else if (is<CSSTransition>(animation)) {
139         auto& transition = downcast<CSSTransition>(animation);
140         if (!removeCSSTransitionFromMap(transition, element, m_elementToRunningCSSTransitionByCSSPropertyID))
141             removeCSSTransitionFromMap(transition, element, m_elementToCompletedCSSTransitionByCSSPropertyID);
142     }
143 }
144
145 Vector<RefPtr<WebAnimation>> AnimationTimeline::animationsForElement(Element& element) const
146 {
147     Vector<RefPtr<WebAnimation>> animations;
148     if (m_elementToCSSTransitionsMap.contains(&element)) {
149         const auto& cssTransitions = m_elementToCSSTransitionsMap.get(&element);
150         animations.appendRange(cssTransitions.begin(), cssTransitions.end());
151     }
152     if (m_elementToCSSAnimationsMap.contains(&element)) {
153         const auto& cssAnimations = m_elementToCSSAnimationsMap.get(&element);
154         animations.appendRange(cssAnimations.begin(), cssAnimations.end());
155     }
156     if (m_elementToAnimationsMap.contains(&element)) {
157         const auto& webAnimations = m_elementToAnimationsMap.get(&element);
158         animations.appendRange(webAnimations.begin(), webAnimations.end());
159     }
160     return animations;
161 }
162
163 void AnimationTimeline::removeAnimationsForElement(Element& element)
164 {
165     for (auto& animation : animationsForElement(element)) {
166         animation->prepareAnimationForRemoval();
167         removeAnimation(animation.releaseNonNull());
168     }
169 }
170
171 void AnimationTimeline::updateCSSAnimationsForElement(Element& element, const RenderStyle* currentStyle, const RenderStyle& afterChangeStyle)
172 {
173     // In case this element is newly getting a "display: none" we need to cancel all of its animations and disregard new ones.
174     if (currentStyle && currentStyle->hasAnimations() && currentStyle->display() != DisplayType::None && afterChangeStyle.display() == DisplayType::None) {
175         if (m_elementToCSSAnimationByName.contains(&element)) {
176             for (const auto& cssAnimationsByNameMapItem : m_elementToCSSAnimationByName.take(&element))
177                 cancelOrRemoveDeclarativeAnimation(cssAnimationsByNameMapItem.value);
178         }
179         return;
180     }
181
182     if (currentStyle && currentStyle->hasAnimations() && afterChangeStyle.hasAnimations() && *(currentStyle->animations()) == *(afterChangeStyle.animations()))
183         return;
184
185     // First, compile the list of animation names that were applied to this element up to this point.
186     HashSet<String> namesOfPreviousAnimations;
187     if (currentStyle && currentStyle->hasAnimations()) {
188         auto* previousAnimations = currentStyle->animations();
189         for (size_t i = 0; i < previousAnimations->size(); ++i) {
190             auto& previousAnimation = previousAnimations->animation(i);
191             if (previousAnimation.isValidAnimation())
192                 namesOfPreviousAnimations.add(previousAnimation.name());
193         }
194     }
195
196     // Create or get the CSSAnimations by animation name map for this element.
197     auto& cssAnimationsByName = m_elementToCSSAnimationByName.ensure(&element, [] {
198         return HashMap<String, RefPtr<CSSAnimation>> { };
199     }).iterator->value;
200
201     if (auto* currentAnimations = afterChangeStyle.animations()) {
202         for (size_t i = 0; i < currentAnimations->size(); ++i) {
203             auto& currentAnimation = currentAnimations->animation(i);
204             auto& name = currentAnimation.name();
205             if (namesOfPreviousAnimations.contains(name)) {
206                 // We've found the name of this animation in our list of previous animations, this means we've already
207                 // created a CSSAnimation object for it and need to ensure that this CSSAnimation is backed by the current
208                 // animation object for this animation name.
209                 cssAnimationsByName.get(name)->setBackingAnimation(currentAnimation);
210             } else if (currentAnimation.isValidAnimation()) {
211                 // Otherwise we are dealing with a new animation name and must create a CSSAnimation for it.
212                 cssAnimationsByName.set(name, CSSAnimation::create(element, currentAnimation, currentStyle, afterChangeStyle));
213             }
214             // Remove the name of this animation from our list since it's now known to be current.
215             namesOfPreviousAnimations.remove(name);
216         }
217     }
218
219     // The animations names left in namesOfPreviousAnimations are now known to no longer apply so we need to
220     // remove the CSSAnimation object created for them.
221     for (const auto& nameOfAnimationToRemove : namesOfPreviousAnimations)
222         cancelOrRemoveDeclarativeAnimation(cssAnimationsByName.take(nameOfAnimationToRemove));
223
224     // Remove the map of CSSAnimations by animation name for this element if it's now empty.
225     if (cssAnimationsByName.isEmpty())
226         m_elementToCSSAnimationByName.remove(&element);
227 }
228
229 RefPtr<WebAnimation> AnimationTimeline::cssAnimationForElementAndProperty(Element& element, CSSPropertyID property)
230 {
231     RefPtr<WebAnimation> matchingAnimation;
232     for (const auto& animation : m_elementToCSSAnimationsMap.get(&element)) {
233         auto* effect = animation->effect();
234         if (is<KeyframeEffectReadOnly>(effect) && downcast<KeyframeEffectReadOnly>(effect)->animatedProperties().contains(property))
235             matchingAnimation = animation;
236     }
237     return matchingAnimation;
238 }
239
240 static bool propertyInStyleMatchesValueForTransitionInMap(CSSPropertyID property, const RenderStyle& style, HashMap<CSSPropertyID, RefPtr<CSSTransition>>& transitions)
241 {
242     if (auto* transition = transitions.get(property)) {
243         if (CSSPropertyAnimation::propertiesEqual(property, &style, &transition->targetStyle()))
244             return true;
245     }
246     return false;
247 }
248
249 static double transitionCombinedDuration(const Animation* transition)
250 {
251     return std::max(0.0, transition->duration()) + transition->delay();
252 }
253
254 static bool transitionMatchesProperty(const Animation& transition, CSSPropertyID property)
255 {
256     auto mode = transition.animationMode();
257     if (mode == Animation::AnimateNone || mode == Animation::AnimateUnknownProperty)
258         return false;
259     if (mode == Animation::AnimateSingleProperty) {
260         auto transitionProperty = transition.property();
261         if (transitionProperty != property) {
262             auto shorthand = shorthandForProperty(transitionProperty);
263             for (size_t i = 0; i < shorthand.length(); ++i) {
264                 if (shorthand.properties()[i] == property)
265                     return true;
266             }
267             return false;
268         }
269     }
270     return true;
271 }
272
273 void AnimationTimeline::updateCSSTransitionsForElement(Element& element, const RenderStyle& currentStyle, const RenderStyle& afterChangeStyle)
274 {
275     // In case this element is newly getting a "display: none" we need to cancel all of its transitions and disregard new ones.
276     if (currentStyle.hasTransitions() && currentStyle.display() != DisplayType::None && afterChangeStyle.display() == DisplayType::None) {
277         if (m_elementToRunningCSSTransitionByCSSPropertyID.contains(&element)) {
278             for (const auto& cssTransitionsByCSSPropertyIDMapItem : m_elementToRunningCSSTransitionByCSSPropertyID.take(&element))
279                 cancelOrRemoveDeclarativeAnimation(cssTransitionsByCSSPropertyIDMapItem.value);
280         }
281         return;
282     }
283
284     // Section 3 "Starting of transitions" from the CSS Transitions Level 1 specification.
285     // https://drafts.csswg.org/css-transitions-1/#starting
286
287     auto& runningTransitionsByProperty = m_elementToRunningCSSTransitionByCSSPropertyID.ensure(&element, [] {
288         return HashMap<CSSPropertyID, RefPtr<CSSTransition>> { };
289     }).iterator->value;
290
291     auto& completedTransitionsByProperty = m_elementToCompletedCSSTransitionByCSSPropertyID.ensure(&element, [] {
292         return HashMap<CSSPropertyID, RefPtr<CSSTransition>> { };
293     }).iterator->value;
294
295     auto numberOfProperties = CSSPropertyAnimation::getNumProperties();
296     for (int propertyIndex = 0; propertyIndex < numberOfProperties; ++propertyIndex) {
297         bool isShorthand;
298         auto property = CSSPropertyAnimation::getPropertyAtIndex(propertyIndex, isShorthand);
299         if (isShorthand)
300             continue;
301
302         const Animation* matchingBackingAnimation = nullptr;
303         if (auto* transitions = afterChangeStyle.transitions()) {
304             for (size_t i = 0; i < transitions->size(); ++i) {
305                 auto& backingAnimation = transitions->animation(i);
306                 if (transitionMatchesProperty(backingAnimation, property))
307                     matchingBackingAnimation = &backingAnimation;
308             }
309         }
310
311         // https://drafts.csswg.org/css-transitions-1/#before-change-style
312         // Define the before-change style as the computed values of all properties on the element as of the previous style change event, except with
313         // any styles derived from declarative animations such as CSS Transitions, CSS Animations, and SMIL Animations updated to the current time.
314         auto existingAnimation = cssAnimationForElementAndProperty(element, property);
315         const auto& beforeChangeStyle = existingAnimation ? downcast<CSSAnimation>(existingAnimation.get())->unanimatedStyle() : currentStyle;
316
317         if (!runningTransitionsByProperty.contains(property)
318             && !CSSPropertyAnimation::propertiesEqual(property, &beforeChangeStyle, &afterChangeStyle)
319             && CSSPropertyAnimation::canPropertyBeInterpolated(property, &beforeChangeStyle, &afterChangeStyle)
320             && !propertyInStyleMatchesValueForTransitionInMap(property, afterChangeStyle, completedTransitionsByProperty)
321             && matchingBackingAnimation && transitionCombinedDuration(matchingBackingAnimation) > 0) {
322             // 1. If all of the following are true:
323             //   - the element does not have a running transition for the property,
324             //   - the before-change style is different from and can be interpolated with the after-change style for that property,
325             //   - the element does not have a completed transition for the property or the end value of the completed transition is different from the after-change style for the property,
326             //   - there is a matching transition-property value, and
327             //   - the combined duration is greater than 0s,
328
329             // then implementations must remove the completed transition (if present) from the set of completed transitions
330             completedTransitionsByProperty.remove(property);
331
332             // and start a transition whose:
333             //   - start time is the time of the style change event plus the matching transition delay,
334             //   - end time is the start time plus the matching transition duration,
335             //   - start value is the value of the transitioning property in the before-change style,
336             //   - end value is the value of the transitioning property in the after-change style,
337             //   - reversing-adjusted start value is the same as the start value, and
338             //   - reversing shortening factor is 1.
339             auto delay = Seconds(matchingBackingAnimation->delay());
340             auto duration = Seconds(matchingBackingAnimation->duration());
341             auto& reversingAdjustedStartStyle = beforeChangeStyle;
342             auto reversingShorteningFactor = 1;
343             runningTransitionsByProperty.set(property, CSSTransition::create(element, property, *matchingBackingAnimation, &beforeChangeStyle, afterChangeStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor));
344         } else if (completedTransitionsByProperty.contains(property) && !propertyInStyleMatchesValueForTransitionInMap(property, afterChangeStyle, completedTransitionsByProperty)) {
345             // 2. Otherwise, if the element has a completed transition for the property and the end value of the completed transition is different from
346             //    the after-change style for the property, then implementations must remove the completed transition from the set of completed transitions.
347             completedTransitionsByProperty.remove(property);
348         }
349
350         bool hasRunningTransition = runningTransitionsByProperty.contains(property);
351         if ((hasRunningTransition || completedTransitionsByProperty.contains(property)) && !matchingBackingAnimation) {
352             // 3. If the element has a running transition or completed transition for the property, and there is not a matching transition-property
353             //    value, then implementations must cancel the running transition or remove the completed transition from the set of completed transitions.
354             if (hasRunningTransition)
355                 runningTransitionsByProperty.take(property)->cancel();
356             else
357                 completedTransitionsByProperty.remove(property);
358         }
359
360         if (matchingBackingAnimation && runningTransitionsByProperty.contains(property) && !propertyInStyleMatchesValueForTransitionInMap(property, afterChangeStyle, runningTransitionsByProperty)) {
361             auto previouslyRunningTransition = runningTransitionsByProperty.take(property);
362             auto& previouslyRunningTransitionCurrentStyle = previouslyRunningTransition->currentStyle();
363             // 4. If the element has a running transition for the property, there is a matching transition-property value, and the end value of the running
364             //    transition is not equal to the value of the property in the after-change style, then:
365             if (CSSPropertyAnimation::propertiesEqual(property, &previouslyRunningTransitionCurrentStyle, &afterChangeStyle) || !CSSPropertyAnimation::canPropertyBeInterpolated(property, &currentStyle, &afterChangeStyle)) {
366                 // 1. If the current value of the property in the running transition is equal to the value of the property in the after-change style,
367                 //    or if these two values cannot be interpolated, then implementations must cancel the running transition.
368                 previouslyRunningTransition->cancel();
369             } else if (transitionCombinedDuration(matchingBackingAnimation) <= 0.0 || !CSSPropertyAnimation::canPropertyBeInterpolated(property, &previouslyRunningTransitionCurrentStyle, &afterChangeStyle)) {
370                 // 2. Otherwise, if the combined duration is less than or equal to 0s, or if the current value of the property in the running transition
371                 //    cannot be interpolated with the value of the property in the after-change style, then implementations must cancel the running transition.
372                 previouslyRunningTransition->cancel();
373             } else if (CSSPropertyAnimation::propertiesEqual(property, &previouslyRunningTransition->reversingAdjustedStartStyle(), &afterChangeStyle)) {
374                 // 3. Otherwise, if the reversing-adjusted start value of the running transition is the same as the value of the property in the after-change
375                 //    style (see the section on reversing of transitions for why these case exists), implementations must cancel the running transition
376                 previouslyRunningTransition->cancel();
377
378                 // and start a new transition whose:
379                 //   - reversing-adjusted start value is the end value of the running transition,
380                 //   - reversing shortening factor is the absolute value, clamped to the range [0, 1], of the sum of:
381                 //       1. the output of the timing function of the old transition at the time of the style change event, times the reversing shortening factor of the old transition
382                 //       2. 1 minus the reversing shortening factor of the old transition.
383                 //   - start time is the time of the style change event plus:
384                 //       1. if the matching transition delay is nonnegative, the matching transition delay, or
385                 //       2. if the matching transition delay is negative, the product of the new transition’s reversing shortening factor and the matching transition delay,
386                 //   - end time is the start time plus the product of the matching transition duration and the new transition’s reversing shortening factor,
387                 //   - start value is the current value of the property in the running transition,
388                 //   - end value is the value of the property in the after-change style
389                 auto& reversingAdjustedStartStyle = previouslyRunningTransition->targetStyle();
390                 double transformedProgress = 1;
391                 if (auto* effect = previouslyRunningTransition->effect())
392                     transformedProgress = effect->iterationProgress().value_or(transformedProgress);
393                 auto reversingShorteningFactor = std::max(std::min(((transformedProgress * previouslyRunningTransition->reversingShorteningFactor()) + (1 - previouslyRunningTransition->reversingShorteningFactor())), 1.0), 0.0);
394                 auto delay = matchingBackingAnimation->delay() < 0 ? Seconds(matchingBackingAnimation->delay()) * reversingShorteningFactor : Seconds(matchingBackingAnimation->delay());
395                 auto duration = Seconds(matchingBackingAnimation->duration()) * reversingShorteningFactor;
396                 runningTransitionsByProperty.set(property, CSSTransition::create(element, property, *matchingBackingAnimation, &previouslyRunningTransitionCurrentStyle, afterChangeStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor));
397             } else {
398                 // 4. Otherwise, implementations must cancel the running transition
399                 previouslyRunningTransition->cancel();
400
401                 // and start a new transition whose:
402                 //   - start time is the time of the style change event plus the matching transition delay,
403                 //   - end time is the start time plus the matching transition duration,
404                 //   - start value is the current value of the property in the running transition,
405                 //   - end value is the value of the property in the after-change style,
406                 //   - reversing-adjusted start value is the same as the start value, and
407                 //   - reversing shortening factor is 1.
408                 auto delay = Seconds(matchingBackingAnimation->delay());
409                 auto duration = Seconds(matchingBackingAnimation->duration());
410                 auto& reversingAdjustedStartStyle = currentStyle;
411                 auto reversingShorteningFactor = 1;
412                 runningTransitionsByProperty.set(property, CSSTransition::create(element, property, *matchingBackingAnimation, &previouslyRunningTransitionCurrentStyle, afterChangeStyle, delay, duration, reversingAdjustedStartStyle, reversingShorteningFactor));
413             }
414         }
415     }
416
417 }
418
419 void AnimationTimeline::cancelOrRemoveDeclarativeAnimation(RefPtr<DeclarativeAnimation> animation)
420 {
421     auto phase = animation->effect()->phase();
422     if (phase != AnimationEffectReadOnly::Phase::Idle && phase != AnimationEffectReadOnly::Phase::After)
423         animation->cancel();
424     else
425         removeAnimation(animation.releaseNonNull());
426 }
427
428 String AnimationTimeline::description()
429 {
430     TextStream stream;
431     int count = 1;
432     stream << (m_classType == DocumentTimelineClass ? "DocumentTimeline" : "AnimationTimeline") << " with " << m_animations.size() << " animations:";
433     stream << "\n";
434     for (const auto& animation : m_animations) {
435         writeIndent(stream, 1);
436         stream << count << ". " << animation->description();
437         stream << "\n";
438         count++;
439     }
440     return stream.release();
441 }
442
443 } // namespace WebCore