浏览代码

LibWeb: Start transitions when affected properties change

Co-authored-by: Matthew Olsson <matthewcolsson@gmail.com>
Sam Atkins 9 月之前
父节点
当前提交
a1fca1a7f3

+ 5 - 0
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

+ 48 - 0
Tests/LibWeb/Text/input/css/transition-basics.html

@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<html>
+<script src="../include.js"></script>
+<style>
+    #box {
+        width: 200px;
+        height: 200px;
+        background-color: red;
+        position: absolute;
+        left: 0;
+        top: 0;
+
+        transition: left 1s linear;
+    }
+
+    #box.move {
+        left: 200px;
+    }
+</style>
+<div id="box"></div>
+<script>
+    asyncTest(async done => {
+        let box = document.getElementById("box");
+        println(`left starts at: ${getComputedStyle(box).left}`);
+        box.classList.add("move");
+        setTimeout(() => {
+            let left = parseFloat(getComputedStyle(box).left);
+            println(`shortly after starting, left is >= 0px? ${left >= 0} and < 200px? ${left < 200}`);
+        }, 1);
+        setTimeout(() => {
+            let left = parseFloat(getComputedStyle(box).left);
+            println(`half-way through, left is > 0px? ${left > 0} and < 200px? ${left < 200}`);
+        }, 500);
+        setTimeout(() => {
+            let left = parseFloat(getComputedStyle(box).left);
+            println(`near the end, left is > 0px? ${left > 0} and < 200px? ${left < 200}`);
+        }, 950);
+        setTimeout(() => {
+            // FIXME: This is a hack to make it recompute the style, otherwise we get 0px for the left value.
+            //        Figure out why!
+            box.style.top = "0px";
+
+            println(`after the transition, left is: ${getComputedStyle(box).left}`);
+            done();
+        }, 1050);
+    });
+</script>
+</html>

+ 68 - 4
Userland/Libraries/LibWeb/CSS/CSSTransition.cpp

@@ -1,22 +1,30 @@
 /*
- * Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>.
+ * Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
 
+#include <LibWeb/Animations/DocumentTimeline.h>
 #include <LibWeb/Bindings/CSSTransitionPrototype.h>
 #include <LibWeb/Bindings/Intrinsics.h>
 #include <LibWeb/CSS/CSSStyleDeclaration.h>
 #include <LibWeb/CSS/CSSTransition.h>
+#include <LibWeb/CSS/Interpolation.h>
+#include <LibWeb/DOM/Document.h>
 #include <LibWeb/DOM/Element.h>
+#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
 
 namespace Web::CSS {
 
 JS_DEFINE_ALLOCATOR(CSSTransition);
 
-JS::NonnullGCPtr<CSSTransition> CSSTransition::create(JS::Realm& realm, PropertyID property_id, size_t transition_generation)
+JS::NonnullGCPtr<CSSTransition> CSSTransition::start_a_transition(DOM::Element& element, PropertyID property_id, size_t transition_generation,
+    double start_time, double end_time, NonnullRefPtr<CSSStyleValue const> start_value, NonnullRefPtr<CSSStyleValue const> end_value,
+    NonnullRefPtr<CSSStyleValue const> reversing_adjusted_start_value, double reversing_shortening_factor)
 {
-    return realm.heap().allocate<CSSTransition>(realm, realm, property_id, transition_generation);
+    auto& realm = element.realm();
+    return realm.heap().allocate<CSSTransition>(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<int> CSSTransition::class_specific_composite_order(JS::NonnullGCPtr<Ani
     return {};
 }
 
-CSSTransition::CSSTransition(JS::Realm& realm, PropertyID property_id, size_t transition_generation)
+CSSTransition::CSSTransition(JS::Realm& realm, DOM::Element& element, PropertyID property_id, size_t transition_generation,
+    double start_time, double end_time, NonnullRefPtr<CSSStyleValue const> start_value, NonnullRefPtr<CSSStyleValue const> end_value,
+    NonnullRefPtr<CSSStyleValue const> 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<CSSStyleValue const> 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;
 }
 
 }

+ 39 - 6
Userland/Libraries/LibWeb/CSS/CSSTransition.h

@@ -1,5 +1,6 @@
 /*
- * Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>.
+ * Copyright (c) 2024, Matthew Olsson <mattco@serenityos.org>
+ * Copyright (c) 2024, Sam Atkins <sam@ladybird.org>
  *
  * SPDX-License-Identifier: BSD-2-Clause
  */
@@ -9,6 +10,7 @@
 #include <LibWeb/Animations/Animation.h>
 #include <LibWeb/CSS/CSSStyleValue.h>
 #include <LibWeb/CSS/PropertyID.h>
+#include <LibWeb/CSS/StyleValues/EasingStyleValue.h>
 #include <LibWeb/CSS/Time.h>
 
 namespace Web::CSS {
@@ -18,18 +20,29 @@ class CSSTransition : public Animations::Animation {
     JS_DECLARE_ALLOCATOR(CSSTransition);
 
 public:
-    static JS::NonnullGCPtr<CSSTransition> create(JS::Realm&, PropertyID, size_t transition_generation);
+    static JS::NonnullGCPtr<CSSTransition> start_a_transition(DOM::Element&, PropertyID, size_t transition_generation,
+        double start_time, double end_time, NonnullRefPtr<CSSStyleValue const> start_value, NonnullRefPtr<CSSStyleValue const> end_value,
+        NonnullRefPtr<CSSStyleValue const> reversing_adjusted_start_value, double reversing_shortening_factor);
 
     StringView transition_property() const { return string_from_property_id(m_transition_property); }
 
-    JS::GCPtr<CSS::CSSStyleDeclaration const> cached_declaration() const { return m_cached_declaration; }
-    void set_cached_declaration(JS::GCPtr<CSS::CSSStyleDeclaration const> declaration) { m_cached_declaration = declaration; }
-
     virtual Animations::AnimationClass animation_class() const override;
     virtual Optional<int> class_specific_composite_order(JS::NonnullGCPtr<Animations::Animation> other) const override;
 
+    double transition_start_time() const { return m_start_time; }
+    double transition_end_time() const { return m_end_time; }
+    NonnullRefPtr<CSSStyleValue const> transition_start_value() const { return m_start_value; }
+    NonnullRefPtr<CSSStyleValue const> transition_end_value() const { return m_end_value; }
+    NonnullRefPtr<CSSStyleValue const> 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<CSSStyleValue const> 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<CSSStyleValue const> start_value, NonnullRefPtr<CSSStyleValue const> end_value,
+        NonnullRefPtr<CSSStyleValue const> 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<CSS::CSSStyleValue const> m_start_value;
+
+    // https://drafts.csswg.org/css-transitions/#transition-end-value
+    NonnullRefPtr<CSS::CSSStyleValue const> m_end_value;
+
+    // https://drafts.csswg.org/css-transitions/#transition-reversing-adjusted-start-value
+    NonnullRefPtr<CSS::CSSStyleValue const> m_reversing_adjusted_start_value;
+
+    // https://drafts.csswg.org/css-transitions/#transition-reversing-shortening-factor
+    double m_reversing_shortening_factor;
+
+    JS::NonnullGCPtr<Animations::KeyframeEffect> m_keyframe_effect;
+
     JS::GCPtr<CSS::CSSStyleDeclaration const> m_cached_declaration;
 };
 

+ 16 - 0
Userland/Libraries/LibWeb/CSS/Interpolation.cpp

@@ -72,6 +72,22 @@ ValueComparingRefPtr<CSSStyleValue const> 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<CSSStyleValue const> interpolate_transform(DOM::Element& element, CSSStyleValue const& from, CSSStyleValue const& to, float delta)
 {

+ 3 - 0
Userland/Libraries/LibWeb/CSS/Interpolation.h

@@ -13,6 +13,9 @@ namespace Web::CSS {
 
 ValueComparingRefPtr<CSSStyleValue const> 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<CSSStyleValue const> interpolate_value(DOM::Element&, CSSStyleValue const& from, CSSStyleValue const& to, float delta);
 NonnullRefPtr<CSSStyleValue const> interpolate_box_shadow(DOM::Element&, CSSStyleValue const& from, CSSStyleValue const& to, float delta);
 RefPtr<CSSStyleValue const> interpolate_transform(DOM::Element&, CSSStyleValue const& from, CSSStyleValue const& to, float delta);

+ 200 - 1
Userland/Libraries/LibWeb/CSS/StyleComputer.cpp

@@ -32,6 +32,7 @@
 #include <LibWeb/CSS/CSSLayerBlockRule.h>
 #include <LibWeb/CSS/CSSLayerStatementRule.h>
 #include <LibWeb/CSS/CSSStyleRule.h>
+#include <LibWeb/CSS/CSSTransition.h>
 #include <LibWeb/CSS/Interpolation.h>
 #include <LibWeb/CSS/Parser/Parser.h>
 #include <LibWeb/CSS/SelectorEngine.h>
@@ -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<Selector::PseudoElement::Type> 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<CSS::PropertyID>(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<CSS::Selector::PseudoElement::Type> 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<CSS::Selector::PseudoElement::Type> pseudo_element)
@@ -2197,6 +2392,10 @@ RefPtr<StyleProperties> 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;
 }
 

+ 1 - 0
Userland/Libraries/LibWeb/CSS/StyleComputer.h

@@ -174,6 +174,7 @@ private:
     void compute_font(StyleProperties&, DOM::Element const*, Optional<CSS::Selector::PseudoElement::Type>) const;
     void compute_math_depth(StyleProperties&, DOM::Element const*, Optional<CSS::Selector::PseudoElement::Type>) const;
     void compute_defaulted_values(StyleProperties&, DOM::Element const*, Optional<CSS::Selector::PseudoElement::Type>) const;
+    void start_needed_transitions(StyleProperties const& old_style, StyleProperties& new_style, DOM::Element&, Optional<Selector::PseudoElement::Type>) const;
     void absolutize_values(StyleProperties&) const;
     void resolve_effective_overflow_values(StyleProperties&) const;
     void transform_box_type_if_needed(StyleProperties&, DOM::Element const&, Optional<CSS::Selector::PseudoElement::Type>) const;