diff --git a/Tests/LibWeb/Text/expected/css/transition-basics.txt b/Tests/LibWeb/Text/expected/css/transition-basics.txt new file mode 100644 index 00000000000..224848202fd --- /dev/null +++ b/Tests/LibWeb/Text/expected/css/transition-basics.txt @@ -0,0 +1,5 @@ + left starts at: 0px +shortly after starting, left is >= 0px? true and < 200px? true +half-way through, left is > 0px? true and < 200px? true +near the end, left is > 0px? true and < 200px? true +after the transition, left is: 200px diff --git a/Tests/LibWeb/Text/input/css/transition-basics.html b/Tests/LibWeb/Text/input/css/transition-basics.html new file mode 100644 index 00000000000..3571f4d0be5 --- /dev/null +++ b/Tests/LibWeb/Text/input/css/transition-basics.html @@ -0,0 +1,48 @@ + + + + +
+ + diff --git a/Userland/Libraries/LibWeb/CSS/CSSTransition.cpp b/Userland/Libraries/LibWeb/CSS/CSSTransition.cpp index 8a913915b0f..4de6f14e322 100644 --- a/Userland/Libraries/LibWeb/CSS/CSSTransition.cpp +++ b/Userland/Libraries/LibWeb/CSS/CSSTransition.cpp @@ -1,22 +1,30 @@ /* - * Copyright (c) 2024, Matthew Olsson . + * Copyright (c) 2024, Matthew Olsson + * Copyright (c) 2024, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ +#include #include #include #include #include +#include +#include #include +#include namespace Web::CSS { JS_DEFINE_ALLOCATOR(CSSTransition); -JS::NonnullGCPtr CSSTransition::create(JS::Realm& realm, PropertyID property_id, size_t transition_generation) +JS::NonnullGCPtr CSSTransition::start_a_transition(DOM::Element& element, PropertyID property_id, size_t transition_generation, + double start_time, double end_time, NonnullRefPtr start_value, NonnullRefPtr end_value, + NonnullRefPtr reversing_adjusted_start_value, double reversing_shortening_factor) { - return realm.heap().allocate(realm, realm, property_id, transition_generation); + auto& realm = element.realm(); + return realm.heap().allocate(realm, realm, element, property_id, transition_generation, start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor); } Animations::AnimationClass CSSTransition::animation_class() const @@ -67,16 +75,53 @@ Optional CSSTransition::class_specific_composite_order(JS::NonnullGCPtr start_value, NonnullRefPtr end_value, + NonnullRefPtr reversing_adjusted_start_value, double reversing_shortening_factor) : Animations::Animation(realm) , m_transition_property(property_id) , m_transition_generation(transition_generation) + , m_start_time(start_time) + , m_end_time(end_time) + , m_start_value(move(start_value)) + , m_end_value(move(end_value)) + , m_reversing_adjusted_start_value(move(reversing_adjusted_start_value)) + , m_reversing_shortening_factor(reversing_shortening_factor) + , m_keyframe_effect(Animations::KeyframeEffect::create(realm)) { // FIXME: // Transitions generated using the markup defined in this specification are not added to the global animation list // when they are created. Instead, these animations are appended to the global animation list at the first moment // when they transition out of the idle play state after being disassociated from their owning element. Transitions // that have been disassociated from their owning element but are still idle do not have a defined composite order. + + set_start_time(start_time - element.document().timeline()->current_time().value()); + + // Construct a KeyframesEffect for our animation + m_keyframe_effect->set_target(&element); + m_keyframe_effect->set_start_delay(start_time); + m_keyframe_effect->set_iteration_duration(m_end_time - start_time); + m_keyframe_effect->set_timing_function(element.property_transition_attributes(property_id)->timing_function); + + auto key_frame_set = adopt_ref(*new Animations::KeyframeEffect::KeyFrameSet); + Animations::KeyframeEffect::KeyFrameSet::ResolvedKeyFrame initial_keyframe; + initial_keyframe.properties.set(property_id, m_start_value); + + Animations::KeyframeEffect::KeyFrameSet::ResolvedKeyFrame final_keyframe; + final_keyframe.properties.set(property_id, m_end_value); + + key_frame_set->keyframes_by_key.insert(0, initial_keyframe); + key_frame_set->keyframes_by_key.insert(100 * Animations::KeyframeEffect::AnimationKeyFrameKeyScaleFactor, final_keyframe); + + m_keyframe_effect->set_key_frame_set(key_frame_set); + set_timeline(element.document().timeline()); + set_owning_element(element); + set_effect(m_keyframe_effect); + element.associate_with_animation(*this); + element.set_transition(m_transition_property, *this); + + HTML::TemporaryExecutionContext context(element.document().relevant_settings_object()); + play().release_value_but_fixme_should_propagate_errors(); } void CSSTransition::initialize(JS::Realm& realm) @@ -89,6 +134,25 @@ void CSSTransition::visit_edges(Cell::Visitor& visitor) { Base::visit_edges(visitor); visitor.visit(m_cached_declaration); + visitor.visit(m_keyframe_effect); +} + +double CSSTransition::timing_function_output_at_time(double t) const +{ + auto progress = (t - transition_start_time()) / (transition_end_time() - transition_start_time()); + // FIXME: Is this before_flag value correct? + bool before_flag = t < transition_start_time(); + return m_keyframe_effect->timing_function().evaluate_at(progress, before_flag); +} + +NonnullRefPtr CSSTransition::value_at_time(double t) const +{ + // https://drafts.csswg.org/css-transitions/#application + auto progress = timing_function_output_at_time(t); + auto result = interpolate_property(*m_keyframe_effect->target(), m_transition_property, m_start_value, m_end_value, progress); + if (result) + return result.release_nonnull(); + return m_start_value; } } diff --git a/Userland/Libraries/LibWeb/CSS/CSSTransition.h b/Userland/Libraries/LibWeb/CSS/CSSTransition.h index 8eac75a3c8a..ac017e208f9 100644 --- a/Userland/Libraries/LibWeb/CSS/CSSTransition.h +++ b/Userland/Libraries/LibWeb/CSS/CSSTransition.h @@ -1,5 +1,6 @@ /* - * Copyright (c) 2024, Matthew Olsson . + * Copyright (c) 2024, Matthew Olsson + * Copyright (c) 2024, Sam Atkins * * SPDX-License-Identifier: BSD-2-Clause */ @@ -9,6 +10,7 @@ #include #include #include +#include #include namespace Web::CSS { @@ -18,18 +20,29 @@ class CSSTransition : public Animations::Animation { JS_DECLARE_ALLOCATOR(CSSTransition); public: - static JS::NonnullGCPtr create(JS::Realm&, PropertyID, size_t transition_generation); + static JS::NonnullGCPtr start_a_transition(DOM::Element&, PropertyID, size_t transition_generation, + double start_time, double end_time, NonnullRefPtr start_value, NonnullRefPtr end_value, + NonnullRefPtr reversing_adjusted_start_value, double reversing_shortening_factor); StringView transition_property() const { return string_from_property_id(m_transition_property); } - JS::GCPtr cached_declaration() const { return m_cached_declaration; } - void set_cached_declaration(JS::GCPtr declaration) { m_cached_declaration = declaration; } - virtual Animations::AnimationClass animation_class() const override; virtual Optional class_specific_composite_order(JS::NonnullGCPtr other) const override; + double transition_start_time() const { return m_start_time; } + double transition_end_time() const { return m_end_time; } + NonnullRefPtr transition_start_value() const { return m_start_value; } + NonnullRefPtr transition_end_value() const { return m_end_value; } + NonnullRefPtr reversing_adjusted_start_value() const { return m_reversing_adjusted_start_value; } + double reversing_shortening_factor() const { return m_reversing_shortening_factor; } + + double timing_function_output_at_time(double t) const; + NonnullRefPtr value_at_time(double t) const; + private: - CSSTransition(JS::Realm&, PropertyID, size_t transition_generation); + CSSTransition(JS::Realm&, DOM::Element&, PropertyID, size_t transition_generation, + double start_time, double end_time, NonnullRefPtr start_value, NonnullRefPtr end_value, + NonnullRefPtr reversing_adjusted_start_value, double reversing_shortening_factor); virtual void initialize(JS::Realm&) override; virtual void visit_edges(Cell::Visitor&) override; @@ -41,6 +54,26 @@ private: // https://drafts.csswg.org/css-transitions-2/#transition-generation size_t m_transition_generation; + // https://drafts.csswg.org/css-transitions/#transition-start-time + double m_start_time; + + // https://drafts.csswg.org/css-transitions/#transition-end-time + double m_end_time; + + // https://drafts.csswg.org/css-transitions/#transition-start-value + NonnullRefPtr m_start_value; + + // https://drafts.csswg.org/css-transitions/#transition-end-value + NonnullRefPtr m_end_value; + + // https://drafts.csswg.org/css-transitions/#transition-reversing-adjusted-start-value + NonnullRefPtr m_reversing_adjusted_start_value; + + // https://drafts.csswg.org/css-transitions/#transition-reversing-shortening-factor + double m_reversing_shortening_factor; + + JS::NonnullGCPtr m_keyframe_effect; + JS::GCPtr m_cached_declaration; }; diff --git a/Userland/Libraries/LibWeb/CSS/Interpolation.cpp b/Userland/Libraries/LibWeb/CSS/Interpolation.cpp index 44832b37629..db90aa056ec 100644 --- a/Userland/Libraries/LibWeb/CSS/Interpolation.cpp +++ b/Userland/Libraries/LibWeb/CSS/Interpolation.cpp @@ -72,6 +72,22 @@ ValueComparingRefPtr interpolate_property(DOM::Element& ele } } +// https://drafts.csswg.org/css-transitions/#transitionable +bool property_values_are_transitionable(PropertyID property_id, CSSStyleValue const& old_value, CSSStyleValue const& new_value) +{ + // When comparing the before-change style and after-change style for a given property, + // the property values are transitionable if they have an animation type that is neither not animatable nor discrete. + + auto animation_type = animation_type_from_longhand_property(property_id); + if (animation_type == AnimationType::None || animation_type == AnimationType::Discrete) + return false; + + // FIXME: Even when a property is transitionable, the two values may not be. The spec uses the example of inset/non-inset shadows. + (void)old_value; + (void)new_value; + return true; +} + // A null return value means the interpolated matrix was not invertible or otherwise invalid RefPtr interpolate_transform(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue const& to, float delta) { diff --git a/Userland/Libraries/LibWeb/CSS/Interpolation.h b/Userland/Libraries/LibWeb/CSS/Interpolation.h index cf17a33c897..59f9e859e12 100644 --- a/Userland/Libraries/LibWeb/CSS/Interpolation.h +++ b/Userland/Libraries/LibWeb/CSS/Interpolation.h @@ -13,6 +13,9 @@ namespace Web::CSS { ValueComparingRefPtr interpolate_property(DOM::Element&, PropertyID, CSSStyleValue const& from, CSSStyleValue const& to, float delta); +// https://drafts.csswg.org/css-transitions/#transitionable +bool property_values_are_transitionable(PropertyID, CSSStyleValue const& old_value, CSSStyleValue const& new_value); + NonnullRefPtr interpolate_value(DOM::Element&, CSSStyleValue const& from, CSSStyleValue const& to, float delta); NonnullRefPtr interpolate_box_shadow(DOM::Element&, CSSStyleValue const& from, CSSStyleValue const& to, float delta); RefPtr interpolate_transform(DOM::Element&, CSSStyleValue const& from, CSSStyleValue const& to, float delta); diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp index 97db8482237..0277d8e2034 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -1294,6 +1295,198 @@ static void compute_transitioned_properties(StyleProperties const& style, DOM::E } } +// https://drafts.csswg.org/css-transitions/#starting +void StyleComputer::start_needed_transitions(StyleProperties const& previous_style, StyleProperties& new_style, DOM::Element& element, Optional pseudo_element) const +{ + // FIXME: Implement transitions for pseudo-elements + (void)pseudo_element; + + // https://drafts.csswg.org/css-transitions/#transition-combined-duration + auto combined_duration = [](Animations::Animatable::TransitionAttributes const& transition_attributes) { + // Define the combined duration of the transition as the sum of max(matching transition duration, 0s) and the matching transition delay. + return max(transition_attributes.duration, 0) + transition_attributes.delay; + }; + + // For each element and property, the implementation must act as follows: + auto style_change_event_time = m_document->timeline()->current_time().value(); + + for (auto i = to_underlying(CSS::first_longhand_property_id); i <= to_underlying(CSS::last_longhand_property_id); ++i) { + auto property_id = static_cast(i); + auto matching_transition_properties = element.property_transition_attributes(property_id); + auto before_change_value = previous_style.property(property_id, StyleProperties::WithAnimationsApplied::No); + auto after_change_value = new_style.property(property_id, StyleProperties::WithAnimationsApplied::No); + + auto existing_transition = element.property_transition(property_id); + bool has_running_transition = existing_transition && !existing_transition->is_finished(); + bool has_completed_transition = existing_transition && existing_transition->is_finished(); + + auto start_a_transition = [&](auto start_time, auto end_time, auto start_value, auto end_value, auto reversing_adjusted_start_value, auto reversing_shortening_factor) { + dbgln("Starting a transition of {} from {} to {}", string_from_property_id(property_id), start_value->to_string(), end_value->to_string()); + + auto transition = CSSTransition::start_a_transition(element, property_id, document().transition_generation(), + start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor); + // Immediately set the property's value to the transition's current value, to prevent single-frame jumps. + new_style.set_animated_property(property_id, transition->value_at_time(style_change_event_time)); + }; + + // 1. If all of the following are true: + if ( + // - the element does not have a running transition for the property, + (!has_running_transition) && + // - the before-change style is different from the after-change style for that property, and the values for the property are transitionable, + (!before_change_value->equals(after_change_value) && property_values_are_transitionable(property_id, before_change_value, after_change_value)) && + // - 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, + (!has_completed_transition || !existing_transition->transition_end_value()->equals(after_change_value)) && + // - there is a matching transition-property value, and + (matching_transition_properties.has_value()) && + // - the combined duration is greater than 0s, + (combined_duration(matching_transition_properties.value()) > 0)) { + + dbgln("Transition step 1."); + + // then implementations must remove the completed transition (if present) from the set of completed transitions + if (has_completed_transition) + element.remove_transition(property_id); + // and start a transition whose: + + // - start time is the time of the style change event plus the matching transition delay, + auto start_time = style_change_event_time + matching_transition_properties->delay; + + // - end time is the start time plus the matching transition duration, + auto end_time = start_time + matching_transition_properties->duration; + + // - start value is the value of the transitioning property in the before-change style, + auto start_value = before_change_value; + + // - end value is the value of the transitioning property in the after-change style, + auto end_value = after_change_value; + + // - reversing-adjusted start value is the same as the start value, and + auto reversing_adjusted_start_value = start_value; + + // - reversing shortening factor is 1. + double reversing_shortening_factor = 1; + + start_a_transition(start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor); + } + + // 2. Otherwise, if the element has a completed transition for the property + // and the end value of the completed transition is different from the after-change style for the property, + // then implementations must remove the completed transition from the set of completed transitions. + else if (has_completed_transition && !existing_transition->transition_end_value()->equals(after_change_value)) { + dbgln("Transition step 2."); + element.remove_transition(property_id); + } + + // 3. If the element has a running transition or completed transition for the property, + // and there is not a matching transition-property value, + if (existing_transition && !matching_transition_properties.has_value()) { + // then implementations must cancel the running transition or remove the completed transition from the set of completed transitions. + dbgln("Transition step 3."); + if (has_running_transition) + existing_transition->cancel(); + else + element.remove_transition(property_id); + } + + // 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 transition is not equal to the value of the property in the after-change style, then: + if (has_running_transition && matching_transition_properties.has_value() && !existing_transition->transition_end_value()->equals(after_change_value)) { + dbgln("Transition step 4. existing end value = {}, after change value = {}", existing_transition->transition_end_value()->to_string(), after_change_value->to_string()); + // 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, + // or if these two values are not transitionable, + // then implementations must cancel the running transition. + auto current_value = existing_transition->value_at_time(style_change_event_time); + if (current_value->equals(after_change_value) || !property_values_are_transitionable(property_id, current_value, after_change_value)) { + dbgln("Transition step 4.1"); + existing_transition->cancel(); + } + + // 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 is not transitionable with the value of the property in the after-change style, + // then implementations must cancel the running transition. + else if ((combined_duration(matching_transition_properties.value()) <= 0) + || !property_values_are_transitionable(property_id, current_value, after_change_value)) { + dbgln("Transition step 4.2"); + existing_transition->cancel(); + } + + // 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 style + // (see the section on reversing of transitions for why these case exists), + else if (existing_transition->reversing_adjusted_start_value()->equals(after_change_value)) { + dbgln("Transition step 4.3"); + // implementations must cancel the running transition and start a new transition whose: + existing_transition->cancel(); + // AD-HOC: Remove the cancelled transition, otherwise it breaks the invariant that there is only one + // running or completed transition for a property at once. + element.remove_transition(property_id); + + // - reversing-adjusted start value is the end value of the running transition, + auto reversing_adjusted_start_value = existing_transition->transition_end_value(); + + // - reversing shortening factor is the absolute value, clamped to the range [0, 1], of the sum of: + // 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 + auto term_1 = existing_transition->timing_function_output_at_time(style_change_event_time) * existing_transition->reversing_shortening_factor(); + // 2. 1 minus the reversing shortening factor of the old transition. + auto term_2 = 1 - existing_transition->reversing_shortening_factor(); + double reversing_shortening_factor = clamp(abs(term_1 + term_2), 0.0, 1.0); + + // - start time is the time of the style change event plus: + // 1. if the matching transition delay is nonnegative, the matching transition delay, or + // 2. if the matching transition delay is negative, the product of the new transition’s reversing shortening factor and the matching transition delay, + auto start_time = style_change_event_time + + (matching_transition_properties->delay >= 0 + ? (matching_transition_properties->delay) + : (reversing_shortening_factor * matching_transition_properties->delay)); + + // - end time is the start time plus the product of the matching transition duration and the new transition’s reversing shortening factor, + auto end_time = start_time + (matching_transition_properties->duration * reversing_shortening_factor); + + // - start value is the current value of the property in the running transition, + auto start_value = current_value; + + // - end value is the value of the property in the after-change style, + auto end_value = after_change_value; + + start_a_transition(start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor); + } + + // 4. Otherwise, + else { + dbgln("Transition step 4.4"); + // implementations must cancel the running transition and start a new transition whose: + existing_transition->cancel(); + // AD-HOC: Remove the cancelled transition, otherwise it breaks the invariant that there is only one + // running or completed transition for a property at once. + element.remove_transition(property_id); + + // - start time is the time of the style change event plus the matching transition delay, + auto start_time = style_change_event_time + matching_transition_properties->delay; + + // - end time is the start time plus the matching transition duration, + auto end_time = start_time + matching_transition_properties->duration; + + // - start value is the current value of the property in the running transition, + auto start_value = current_value; + + // - end value is the value of the property in the after-change style, + auto end_value = after_change_value; + + // - reversing-adjusted start value is the same as the start value, and + auto reversing_adjusted_start_value = start_value; + + // - reversing shortening factor is 1. + double reversing_shortening_factor = 1; + + start_a_transition(start_time, end_time, start_value, end_value, reversing_adjusted_start_value, reversing_shortening_factor); + } + } + } +} + // https://www.w3.org/TR/css-cascade/#cascading // https://drafts.csswg.org/css-cascade-5/#layering void StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element& element, Optional pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const @@ -1440,7 +1633,9 @@ void StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element // Important user agent declarations cascade_declarations(style, element, pseudo_element, matching_rule_set.user_agent_rules, CascadeOrigin::UserAgent, Important::Yes); - // FIXME: Transition declarations [css-transitions-1] + // Transition declarations [css-transitions-1] + // Note that we have to do these after finishing computing the style, + // so they're not done here, but as the final step in compute_style_impl() } DOM::Element const* element_to_inherit_style_from(DOM::Element const* element, Optional pseudo_element) @@ -2197,6 +2392,10 @@ RefPtr StyleComputer::compute_style_impl(DOM::Element& element, // 9. Transition declarations [css-transitions-1] // Theoretically this should be part of the cascade, but it works with computed values, which we don't have until now. compute_transitioned_properties(style, element, pseudo_element); + if (auto const* previous_style = element.computed_css_values()) { + start_needed_transitions(*previous_style, style, element, pseudo_element); + } + return style; } diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.h b/Userland/Libraries/LibWeb/CSS/StyleComputer.h index 0ae43b8139d..9d854107343 100644 --- a/Userland/Libraries/LibWeb/CSS/StyleComputer.h +++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.h @@ -174,6 +174,7 @@ private: void compute_font(StyleProperties&, DOM::Element const*, Optional) const; void compute_math_depth(StyleProperties&, DOM::Element const*, Optional) const; void compute_defaulted_values(StyleProperties&, DOM::Element const*, Optional) const; + void start_needed_transitions(StyleProperties const& old_style, StyleProperties& new_style, DOM::Element&, Optional) const; void absolutize_values(StyleProperties&) const; void resolve_effective_overflow_values(StyleProperties&) const; void transform_box_type_if_needed(StyleProperties&, DOM::Element const&, Optional) const;