| // Copyright 2015 The Cobalt Authors. All Rights Reserved. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| #include "cobalt/cssom/css_transition_set.h" | |
| #include <algorithm> | |
| #include <cmath> | |
| #include "base/lazy_instance.h" | |
| #include "cobalt/base/polymorphic_downcast.h" | |
| #include "cobalt/cssom/css_computed_style_data.h" | |
| #include "cobalt/cssom/keyword_value.h" | |
| #include "cobalt/cssom/property_key_list_value.h" | |
| #include "cobalt/cssom/property_value.h" | |
| #include "cobalt/cssom/time_list_value.h" | |
| #include "cobalt/cssom/timing_function_list_value.h" | |
| namespace cobalt { | |
| namespace cssom { | |
| namespace { | |
| // Returns the index at which the property should be animated. If the property | |
| // should not be animated, -1 is returned. | |
| int GetPropertyTransitionIndex( | |
| PropertyKey property, | |
| const scoped_refptr<const PropertyValue>& transition_property_value) { | |
| if (transition_property_value == KeywordValue::GetNone()) { | |
| return -1; | |
| } else { | |
| const PropertyKeyListValue* property_name_list = | |
| base::polymorphic_downcast<const PropertyKeyListValue*>( | |
| transition_property_value.get()); | |
| // The LAST reference to the given property in the transition-property list | |
| // is the one we use to determine the property's index, so start searching | |
| // for it from the back to the front. | |
| // https://www.w3.org/TR/2013/WD-css3-transitions-20131119/#transition-property-property | |
| for (int i = static_cast<int>(property_name_list->value().size()) - 1; | |
| i >= 0; --i) { | |
| PropertyKey current_list_property = | |
| property_name_list->value()[static_cast<size_t>(i)]; | |
| if (current_list_property == kAllProperty || | |
| current_list_property == property) { | |
| return i; | |
| } | |
| } | |
| } | |
| return -1; | |
| } | |
| } // namespace | |
| TransitionSet::TransitionSet(EventHandler* event_handler) | |
| : transitions_(event_handler) {} | |
| void TransitionSet::UpdateTransitions( | |
| const base::TimeDelta& current_time, | |
| const CSSComputedStyleData& source_computed_style, | |
| const CSSComputedStyleData& destination_computed_style) { | |
| const TimeListValue* transition_duration = | |
| base::polymorphic_downcast<const TimeListValue*>( | |
| destination_computed_style.transition_duration().get()); | |
| if (transition_duration->value().size() == 1 && | |
| transition_duration->value()[0] == base::TimeDelta() && | |
| transitions_.IsEmpty()) { | |
| // No need to process transitions if all their durations are set to | |
| // 0, the initial value, so this will happen often. | |
| return; | |
| } | |
| // For each animatable property, check to see if there are any transitions | |
| // assigned to it. If so, check to see if there are any existing transitions | |
| // that must be updated, otherwise introduce new transitions. | |
| const PropertyKeyVector& animatable_properties = GetAnimatableProperties(); | |
| for (PropertyKeyVector::const_iterator iter = animatable_properties.begin(); | |
| iter != animatable_properties.end(); ++iter) { | |
| UpdateTransitionForProperty( | |
| *iter, current_time, source_computed_style.GetPropertyValue(*iter), | |
| destination_computed_style.GetPropertyValue(*iter), | |
| destination_computed_style); | |
| } | |
| } | |
| namespace { | |
| void SetReversingValues( | |
| const base::TimeDelta& current_time, const Transition& old_transition, | |
| const scoped_refptr<PropertyValue>& new_start_value, | |
| const scoped_refptr<PropertyValue>& new_end_value, | |
| scoped_refptr<PropertyValue>* new_reversing_adjusted_start_value, | |
| float* new_reversing_shortening_factor) { | |
| // This value is calculated as explained here: | |
| // https://www.w3.org/TR/css3-transitions/#reversing | |
| // These calculations make a pleasant experience when reversing a transition | |
| // half-way through by making the reverse transition occur over half as much | |
| // time. | |
| if (old_transition.reversing_adjusted_start_value()->Equals(*new_end_value)) { | |
| *new_reversing_shortening_factor = std::min<float>( | |
| 1.0f, | |
| std::max<float>( | |
| 0.0f, std::abs(old_transition.Progress(current_time) * | |
| old_transition.reversing_shortening_factor() + | |
| 1 - old_transition.reversing_shortening_factor()))); | |
| *new_reversing_adjusted_start_value = old_transition.end_value(); | |
| } else { | |
| *new_reversing_adjusted_start_value = new_start_value; | |
| *new_reversing_shortening_factor = 1.0f; | |
| } | |
| } | |
| base::TimeDelta ScaleTimeDelta(const base::TimeDelta& time_delta, float scale) { | |
| return base::TimeDelta::FromMicroseconds(static_cast<int64>( | |
| time_delta.InSecondsF() * scale * base::Time::kMicrosecondsPerSecond)); | |
| } | |
| // Given that a new transition has started on a CSS value that had an active old | |
| // transition occurring on it, this function creates a new transition based on | |
| // the old transition's values (so that we can smoothly transition out of the | |
| // middle of an old transition). | |
| Transition CreateTransitionOverOldTransition( | |
| PropertyKey property, const base::TimeDelta& current_time, | |
| const Transition& old_transition, const base::TimeDelta& duration, | |
| const base::TimeDelta& delay, | |
| const scoped_refptr<TimingFunction>& timing_function, | |
| const scoped_refptr<PropertyValue>& end_value) { | |
| // Since we're updating an old transition, we'll need to know the animated | |
| // CSS style value from the old transition at this point in time. | |
| scoped_refptr<PropertyValue> current_value_within_old_transition = | |
| old_transition.Evaluate(current_time); | |
| // If the new transition is a reversal fo the old transition, we need to | |
| // setup reversing_adjusted_start_value and reversing_shortening_factor so | |
| // that they can be used to reduce the new transition's duration. | |
| scoped_refptr<PropertyValue> new_reversing_adjusted_start_value; | |
| float new_reversing_shortening_factor; | |
| SetReversingValues(current_time, old_transition, | |
| current_value_within_old_transition, end_value, | |
| &new_reversing_adjusted_start_value, | |
| &new_reversing_shortening_factor); | |
| base::TimeDelta with_reversing_delay = | |
| delay < base::TimeDelta() | |
| ? ScaleTimeDelta(delay, new_reversing_shortening_factor) | |
| : delay; | |
| return Transition( | |
| property, current_value_within_old_transition, end_value, current_time, | |
| ScaleTimeDelta(duration, new_reversing_shortening_factor), | |
| with_reversing_delay, timing_function, new_reversing_adjusted_start_value, | |
| new_reversing_shortening_factor); | |
| } | |
| } // namespace | |
| void TransitionSet::UpdateTransitionForProperty( | |
| PropertyKey property, const base::TimeDelta& current_time, | |
| const scoped_refptr<PropertyValue>& start_value, | |
| const scoped_refptr<PropertyValue>& end_value, | |
| const CSSComputedStyleData& transition_style) { | |
| // This method essentially implements the logic defined at | |
| // https://www.w3.org/TR/css3-transitions/#starting | |
| // Get the index of this property in the transition-property list, so we | |
| // can know if the property should be animated (i.e. it is animated if it | |
| // is in the list) and also how to match up transition-duration and other | |
| // transition attributes to it. | |
| int transition_index = GetPropertyTransitionIndex( | |
| property, transition_style.transition_property()); | |
| // The property is only animated if its transition duration is greater than 0. | |
| const TimeListValue* transition_duration = | |
| base::polymorphic_downcast<const TimeListValue*>( | |
| transition_style.transition_duration().get()); | |
| // Get animation parameters by using the transition_index modulo the | |
| // specific property sizes. This is inline with the specification which | |
| // states that list property values should be repeated to calculate values | |
| // for indices larger than the list's size. | |
| base::TimeDelta duration = | |
| transition_index < 0 | |
| ? base::TimeDelta() | |
| : transition_duration->get_item_modulo_size(transition_index); | |
| if (!duration.is_zero()) { | |
| if (!start_value->Equals(*end_value)) { | |
| TimeListValue* delay_list = base::polymorphic_downcast<TimeListValue*>( | |
| transition_style.transition_delay().get()); | |
| const base::TimeDelta& delay = | |
| delay_list->get_item_modulo_size(transition_index); | |
| TimingFunctionListValue* timing_function_list = | |
| base::polymorphic_downcast<TimingFunctionListValue*>( | |
| transition_style.transition_timing_function().get()); | |
| const scoped_refptr<TimingFunction>& timing_function = | |
| timing_function_list->get_item_modulo_size(transition_index); | |
| // The property has been modified and the transition should be animated. | |
| // We now check if an active transition for this property already exists | |
| // or not. | |
| const Transition* existing_transition = | |
| transitions_.GetTransitionForProperty(property); | |
| if (existing_transition && | |
| current_time < existing_transition->EndTime()) { | |
| // A transition is already occurring, so we handle this case a bit | |
| // differently depending on if we're reversing the previous transition | |
| // or starting a completely different one. | |
| transitions_.UpdateTransitionForProperty( | |
| property, | |
| CreateTransitionOverOldTransition( | |
| property, current_time, *existing_transition, duration, delay, | |
| timing_function, end_value), | |
| this); | |
| } else { | |
| // There is no transition on the object currently, so create a new | |
| // one for it. | |
| transitions_.UpdateTransitionForProperty( | |
| property, | |
| Transition(property, start_value, end_value, current_time, duration, | |
| delay, timing_function, start_value, 1.0f), | |
| this); | |
| } | |
| } else { | |
| // Check if there is an existing transition for this property and see | |
| // if it has completed yet. If so, remove it from the list of | |
| // transformations. | |
| const Transition* transition = | |
| transitions_.GetTransitionForProperty(property); | |
| if (transition != NULL) { | |
| if (current_time >= transition->EndTime()) { | |
| transitions_.RemoveTransitionForPropertyIfExists( | |
| property, Transition::kIsNotCanceled); | |
| } | |
| } | |
| } | |
| } else { | |
| // The property is not in the transition-property list or has zero | |
| // duration, thus it should no longer be animated. Remove the transition if | |
| // it exists. It does not generate a transitionend event, it does not pass | |
| // go, it does not collect $200. | |
| transitions_.RemoveTransitionForPropertyIfExists(property, | |
| Transition::kIsCanceled); | |
| } | |
| } | |
| const Transition* TransitionSet::GetTransitionForProperty( | |
| PropertyKey property) const { | |
| return transitions_.GetTransitionForProperty(property); | |
| } | |
| void TransitionSet::Clear() { transitions_.Clear(); } | |
| TransitionSet::TransitionMap::TransitionMap(EventHandler* event_handler) | |
| : event_handler_(event_handler) {} | |
| TransitionSet::TransitionMap::~TransitionMap() { | |
| if (event_handler_ != NULL) { | |
| for (InternalTransitionMap::iterator iter = transitions_.begin(); | |
| iter != transitions_.end(); ++iter) { | |
| event_handler_->OnTransitionRemoved(iter->second, | |
| Transition::kIsCanceled); | |
| } | |
| } | |
| } | |
| const Transition* TransitionSet::TransitionMap::GetTransitionForProperty( | |
| PropertyKey property) const { | |
| InternalTransitionMap::const_iterator found = transitions_.find(property); | |
| if (found == transitions_.end()) return NULL; | |
| return &found->second; | |
| } | |
| void TransitionSet::TransitionMap::UpdateTransitionForProperty( | |
| PropertyKey property, const Transition& transition, | |
| cssom::TransitionSet* transition_set) { | |
| InternalTransitionMap::iterator found = transitions_.find(property); | |
| if (found != transitions_.end()) { | |
| if (event_handler_ != NULL) { | |
| event_handler_->OnTransitionRemoved(found->second, | |
| Transition::kIsCanceled); | |
| } | |
| found->second = transition; | |
| } else { | |
| transitions_.insert(std::make_pair(property, transition)); | |
| } | |
| if (event_handler_ != NULL) { | |
| event_handler_->OnTransitionStarted(transition, transition_set); | |
| } | |
| } | |
| void TransitionSet::TransitionMap::RemoveTransitionForPropertyIfExists( | |
| PropertyKey property, Transition::IsCanceled is_canceled) { | |
| InternalTransitionMap::iterator found = transitions_.find(property); | |
| if (found == transitions_.end()) { | |
| return; | |
| } | |
| if (event_handler_ != NULL) { | |
| event_handler_->OnTransitionRemoved(found->second, is_canceled); | |
| } | |
| transitions_.erase(found); | |
| } | |
| void TransitionSet::TransitionMap::Clear() { | |
| if (event_handler_ != NULL) { | |
| for (auto& transition : transitions_) { | |
| event_handler_->OnTransitionRemoved(transition.second, | |
| Transition::kIsCanceled); | |
| } | |
| } | |
| transitions_.clear(); | |
| } | |
| namespace { | |
| base::LazyInstance<TransitionSet>::DestructorAtExit g_empty_transition_set = | |
| LAZY_INSTANCE_INITIALIZER; | |
| } // namespace | |
| const TransitionSet* TransitionSet::EmptyTransitionSet() { | |
| return &g_empty_transition_set.Get(); | |
| } | |
| } // namespace cssom | |
| } // namespace cobalt |