Browse Source

LibWeb: Add preliminary support for CSS animations

This partially implements CSS-Animations-1 (though there are references
to CSS-Animations-2).
Current limitations:
- Multi-selector keyframes are not supported.
- Most animation properties are ignored.
- Timing functions are not applied.
- Non-absolute values are not interpolated unless the target is also of
  the same non-absolute type (e.g. 10% -> 25%, but not 10% -> 20px).
- The JavaScript interface is left as an exercise for the next poor soul
  looking at this code.

With those said, this commit implements:
- Interpolation for most common types
- Proper keyframe resolution (including the synthetic from-keyframe
  containing the initial state)
- Properly driven animations, and proper style invalidation

Co-Authored-By: Andreas Kling <kling@serenityos.org>
Ali Mohammad Pur 2 years ago
parent
commit
e90752cc21
31 changed files with 1062 additions and 12 deletions
  1. 4 0
      AK/Debug.h.in
  2. 50 0
      Base/res/html/misc/css-animations.html
  3. 1 0
      Base/res/html/misc/welcome.html
  4. 1 0
      Meta/CMake/all_the_debug_macros.cmake
  5. 2 0
      Userland/Libraries/LibWeb/CMakeLists.txt
  6. 6 0
      Userland/Libraries/LibWeb/CSS/CSSConditionRule.cpp
  7. 1 0
      Userland/Libraries/LibWeb/CSS/CSSConditionRule.h
  8. 5 0
      Userland/Libraries/LibWeb/CSS/CSSGroupingRule.cpp
  9. 1 0
      Userland/Libraries/LibWeb/CSS/CSSGroupingRule.h
  10. 30 0
      Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.cpp
  11. 54 0
      Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.h
  12. 7 0
      Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.idl
  13. 36 0
      Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.cpp
  14. 56 0
      Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.h
  15. 13 0
      Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.idl
  16. 2 0
      Userland/Libraries/LibWeb/CSS/CSSRule.h
  17. 2 0
      Userland/Libraries/LibWeb/CSS/CSSRule.idl
  18. 36 0
      Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp
  19. 1 0
      Userland/Libraries/LibWeb/CSS/CSSRuleList.h
  20. 2 1
      Userland/Libraries/LibWeb/CSS/CSSStyleDeclaration.h
  21. 6 0
      Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp
  22. 1 0
      Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h
  23. 12 0
      Userland/Libraries/LibWeb/CSS/Enums.json
  24. 12 0
      Userland/Libraries/LibWeb/CSS/Identifiers.json
  25. 104 0
      Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
  26. 9 2
      Userland/Libraries/LibWeb/CSS/Parser/TokenStream.h
  27. 98 1
      Userland/Libraries/LibWeb/CSS/Properties.json
  28. 428 1
      Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
  29. 70 0
      Userland/Libraries/LibWeb/CSS/StyleComputer.h
  30. 3 0
      Userland/Libraries/LibWeb/Dump.cpp
  31. 9 7
      Userland/Libraries/LibWeb/Forward.h

+ 4 - 0
AK/Debug.h.in

@@ -242,6 +242,10 @@
 #    cmakedefine01 LIBWEB_CSS_DEBUG
 #    cmakedefine01 LIBWEB_CSS_DEBUG
 #endif
 #endif
 
 
+#ifndef LIBWEB_CSS_ANIMATION_DEBUG
+#    cmakedefine01 LIBWEB_CSS_ANIMATION_DEBUG
+#endif
+
 #ifndef LINE_EDITOR_DEBUG
 #ifndef LINE_EDITOR_DEBUG
 #    cmakedefine01 LINE_EDITOR_DEBUG
 #    cmakedefine01 LINE_EDITOR_DEBUG
 #endif
 #endif

+ 50 - 0
Base/res/html/misc/css-animations.html

@@ -0,0 +1,50 @@
+<style>
+.system {
+  position: absolute;
+  width: 100%;
+  height: 100%;
+  background: #000;
+  overflow: hidden;
+}
+.buggie {
+  position: absolute;
+  width: 50%;
+  height: 50%;
+  scale: 50%;
+  opacity: 0;
+  background: url(https://serenityos.org/buggie.png) no-repeat left center;
+  background-size: contain;
+  animation: buggie 10s linear infinite;
+}
+.offset-0 { animation-delay: 0.9s; }
+.offset-1 { animation-delay: 1.7s; }
+.offset-2 { animation-delay: 3.5s; }
+.offset-3 { animation-delay: 4.3s; }
+
+.ladyball {
+  position: absolute;
+  width: 50%;
+  height: 50%;
+  background: url(https://upload.wikimedia.org/wikipedia/commons/thumb/b/b8/LadyBall-SerenityOS.png/240px-LadyBall-SerenityOS.png) no-repeat left center;
+  scale: 50%;
+  animation: ladyball 9s linear infinite;
+}
+@keyframes buggie {
+  0% { transform: translateX(0vw); opacity: 1; }
+  50% { transform: translateX(100vw); opacity: 1; }
+  100% { transform: translateX(0vw); opacity: 1; }
+}
+@keyframes ladyball {
+  0% { transform: translateX(0vw); }
+  50% { transform: translateX(100vw); }
+  100% { transform: translateX(0vw); }
+}
+</style>
+
+<div class=system>
+  <div class="buggie offset-0"></div>
+  <div class="buggie offset-1"></div>
+  <div class="buggie offset-2"></div>
+  <div class="buggie offset-3"></div>
+  <div class="ladyball"></div>
+</div>

+ 1 - 0
Base/res/html/misc/welcome.html

@@ -163,6 +163,7 @@
             <li><a href="inline-node.html">Styling "inline" elements</a></li>
             <li><a href="inline-node.html">Styling "inline" elements</a></li>
             <li><a href="pseudo-elements.html">Pseudo-elements (::before, ::after, etc)</a></li>
             <li><a href="pseudo-elements.html">Pseudo-elements (::before, ::after, etc)</a></li>
             <li><a href="effects_with_opacity_and_transforms.html">Effects with opacity and transforms</a></li>
             <li><a href="effects_with_opacity_and_transforms.html">Effects with opacity and transforms</a></li>
+            <li><a href="css-animations.html">CSS Animations</a></li>
         </ul>
         </ul>
 
 
         <h2>JavaScript/Wasm</h2>
         <h2>JavaScript/Wasm</h2>

+ 1 - 0
Meta/CMake/all_the_debug_macros.cmake

@@ -92,6 +92,7 @@ set(KEYBOARD_SHORTCUTS_DEBUG ON)
 set(KMALLOC_DEBUG ON)
 set(KMALLOC_DEBUG ON)
 set(LANGUAGE_SERVER_DEBUG ON)
 set(LANGUAGE_SERVER_DEBUG ON)
 set(LEXER_DEBUG ON)
 set(LEXER_DEBUG ON)
+set(LIBWEB_CSS_ANIMATION_DEBUG ON)
 set(LIBWEB_CSS_DEBUG ON)
 set(LIBWEB_CSS_DEBUG ON)
 set(LINE_EDITOR_DEBUG ON)
 set(LINE_EDITOR_DEBUG ON)
 set(LOCAL_SOCKET_DEBUG ON)
 set(LOCAL_SOCKET_DEBUG ON)

+ 2 - 0
Userland/Libraries/LibWeb/CMakeLists.txt

@@ -22,6 +22,8 @@ set(SOURCES
     CSS/CSSConditionRule.cpp
     CSS/CSSConditionRule.cpp
     CSS/CSSGroupingRule.cpp
     CSS/CSSGroupingRule.cpp
     CSS/CSSImportRule.cpp
     CSS/CSSImportRule.cpp
+    CSS/CSSKeyframeRule.cpp
+    CSS/CSSKeyframesRule.cpp
     CSS/CSSFontFaceRule.cpp
     CSS/CSSFontFaceRule.cpp
     CSS/CSSMediaRule.cpp
     CSS/CSSMediaRule.cpp
     CSS/CSSRule.cpp
     CSS/CSSRule.cpp

+ 6 - 0
Userland/Libraries/LibWeb/CSS/CSSConditionRule.cpp

@@ -22,6 +22,12 @@ void CSSConditionRule::for_each_effective_style_rule(Function<void(CSSStyleRule
         CSSGroupingRule::for_each_effective_style_rule(callback);
         CSSGroupingRule::for_each_effective_style_rule(callback);
 }
 }
 
 
+void CSSConditionRule::for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const
+{
+    if (condition_matches())
+        CSSGroupingRule::for_each_effective_keyframes_at_rule(callback);
+}
+
 JS::ThrowCompletionOr<void> CSSConditionRule::initialize(JS::Realm& realm)
 JS::ThrowCompletionOr<void> CSSConditionRule::initialize(JS::Realm& realm)
 {
 {
     MUST_OR_THROW_OOM(Base::initialize(realm));
     MUST_OR_THROW_OOM(Base::initialize(realm));

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

@@ -23,6 +23,7 @@ public:
     virtual bool condition_matches() const = 0;
     virtual bool condition_matches() const = 0;
 
 
     virtual void for_each_effective_style_rule(Function<void(CSSStyleRule const&)> const& callback) const override;
     virtual void for_each_effective_style_rule(Function<void(CSSStyleRule const&)> const& callback) const override;
+    virtual void for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const override;
 
 
 protected:
 protected:
     CSSConditionRule(JS::Realm&, CSSRuleList&);
     CSSConditionRule(JS::Realm&, CSSRuleList&);

+ 5 - 0
Userland/Libraries/LibWeb/CSS/CSSGroupingRule.cpp

@@ -54,6 +54,11 @@ void CSSGroupingRule::for_each_effective_style_rule(Function<void(CSSStyleRule c
     m_rules->for_each_effective_style_rule(callback);
     m_rules->for_each_effective_style_rule(callback);
 }
 }
 
 
+void CSSGroupingRule::for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const
+{
+    m_rules->for_each_effective_keyframes_at_rule(callback);
+}
+
 void CSSGroupingRule::set_parent_style_sheet(CSSStyleSheet* parent_style_sheet)
 void CSSGroupingRule::set_parent_style_sheet(CSSStyleSheet* parent_style_sheet)
 {
 {
     CSSRule::set_parent_style_sheet(parent_style_sheet);
     CSSRule::set_parent_style_sheet(parent_style_sheet);

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

@@ -27,6 +27,7 @@ public:
     WebIDL::ExceptionOr<void> delete_rule(u32 index);
     WebIDL::ExceptionOr<void> delete_rule(u32 index);
 
 
     virtual void for_each_effective_style_rule(Function<void(CSSStyleRule const&)> const& callback) const;
     virtual void for_each_effective_style_rule(Function<void(CSSStyleRule const&)> const& callback) const;
+    virtual void for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const;
 
 
     virtual void set_parent_style_sheet(CSSStyleSheet*) override;
     virtual void set_parent_style_sheet(CSSStyleSheet*) override;
 
 

+ 30 - 0
Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.cpp

@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CSSKeyframeRule.h"
+#include <LibWeb/CSS/CSSRuleList.h>
+
+namespace Web::CSS {
+
+void CSSKeyframeRule::visit_edges(Visitor& visitor)
+{
+    Base::visit_edges(visitor);
+    visitor.visit(m_declarations);
+}
+
+JS::ThrowCompletionOr<void> CSSKeyframeRule::initialize(JS::Realm&)
+{
+    return {};
+}
+
+DeprecatedString CSSKeyframeRule::serialized() const
+{
+    StringBuilder builder;
+    builder.appendff("{}% {{ {} }}", key().value(), style()->serialized());
+    return builder.to_deprecated_string();
+}
+
+}

+ 54 - 0
Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.h

@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/NonnullRefPtr.h>
+#include <LibWeb/CSS/CSSRule.h>
+#include <LibWeb/CSS/CSSStyleDeclaration.h>
+#include <LibWeb/CSS/Percentage.h>
+#include <LibWeb/Forward.h>
+#include <LibWeb/WebIDL/ExceptionOr.h>
+
+namespace Web::CSS {
+
+// https://drafts.csswg.org/css-animations/#interface-csskeyframerule
+class CSSKeyframeRule final : public CSSRule {
+    WEB_PLATFORM_OBJECT(CSSKeyframeRule, CSSRule);
+
+public:
+    static WebIDL::ExceptionOr<JS::NonnullGCPtr<CSSKeyframeRule>> create(JS::Realm& realm, CSS::Percentage key, CSSStyleDeclaration& declarations)
+    {
+        return MUST_OR_THROW_OOM(realm.heap().allocate<CSSKeyframeRule>(realm, realm, key, declarations));
+    }
+
+    virtual ~CSSKeyframeRule() = default;
+
+    virtual Type type() const override { return Type::Keyframe; };
+
+    CSS::Percentage key() const { return m_key; }
+    JS::NonnullGCPtr<CSSStyleDeclaration> style() const { return m_declarations; }
+
+private:
+    CSSKeyframeRule(JS::Realm& realm, CSS::Percentage key, CSSStyleDeclaration& declarations)
+        : CSSRule(realm)
+        , m_key(key)
+        , m_declarations(declarations)
+    {
+    }
+
+    virtual void visit_edges(Visitor&) override;
+    virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
+    virtual DeprecatedString serialized() const override;
+
+    CSS::Percentage m_key;
+    JS::NonnullGCPtr<CSSStyleDeclaration> m_declarations;
+};
+
+template<>
+inline bool CSSRule::fast_is<CSSKeyframeRule>() const { return type() == CSSRule::Type::Keyframe; }
+
+}

+ 7 - 0
Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.idl

@@ -0,0 +1,7 @@
+#import <CSS/CSSRule.idl>
+
+[Exposed = Window]
+interface CSSKeyframeRule : CSSRule {
+    attribute CSSOMString keyText;
+    [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
+};

+ 36 - 0
Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.cpp

@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CSSKeyframesRule.h"
+
+namespace Web::CSS {
+
+void CSSKeyframesRule::visit_edges(Visitor& visitor)
+{
+    Base::visit_edges(visitor);
+    for (auto& keyframe : m_keyframes)
+        visitor.visit(keyframe);
+}
+
+JS::ThrowCompletionOr<void> CSSKeyframesRule::initialize(JS::Realm&)
+{
+    return {};
+}
+
+DeprecatedString CSSKeyframesRule::serialized() const
+{
+    StringBuilder builder;
+    builder.appendff("@keyframes \"{}\"", name());
+    builder.append(" { "sv);
+    for (auto& keyframe : keyframes()) {
+        builder.append(keyframe->css_text());
+        builder.append(' ');
+    }
+    builder.append('}');
+    return builder.to_deprecated_string();
+}
+
+}

+ 56 - 0
Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.h

@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur <mpfard@serenityos.org>
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include <AK/FlyString.h>
+#include <AK/NonnullRefPtr.h>
+#include <LibJS/Heap/GCPtr.h>
+#include <LibWeb/CSS/CSSKeyframeRule.h>
+#include <LibWeb/CSS/CSSRule.h>
+#include <LibWeb/Forward.h>
+#include <LibWeb/WebIDL/ExceptionOr.h>
+
+namespace Web::CSS {
+
+// https://drafts.csswg.org/css-animations/#interface-csskeyframesrule
+class CSSKeyframesRule final : public CSSRule {
+    WEB_PLATFORM_OBJECT(CSSKeyframesRule, CSSRule);
+
+public:
+    static WebIDL::ExceptionOr<JS::NonnullGCPtr<CSSKeyframesRule>> create(JS::Realm& realm, FlyString name, Vector<JS::NonnullGCPtr<CSSKeyframeRule>> keyframes)
+    {
+        return MUST_OR_THROW_OOM(realm.heap().allocate<CSSKeyframesRule>(realm, realm, move(name), move(keyframes)));
+    }
+
+    virtual ~CSSKeyframesRule() = default;
+
+    virtual Type type() const override { return Type::Keyframes; };
+
+    Vector<JS::NonnullGCPtr<CSSKeyframeRule>> const& keyframes() const { return m_keyframes; }
+    FlyString const& name() const { return m_name; }
+
+private:
+    CSSKeyframesRule(JS::Realm& realm, FlyString name, Vector<JS::NonnullGCPtr<CSSKeyframeRule>> keyframes)
+        : CSSRule(realm)
+        , m_name(move(name))
+        , m_keyframes(move(keyframes))
+    {
+    }
+
+    virtual void visit_edges(Visitor&) override;
+
+    virtual JS::ThrowCompletionOr<void> initialize(JS::Realm&) override;
+    virtual DeprecatedString serialized() const override;
+
+    FlyString m_name;
+    Vector<JS::NonnullGCPtr<CSSKeyframeRule>> m_keyframes;
+};
+
+template<>
+inline bool CSSRule::fast_is<CSSKeyframesRule>() const { return type() == CSSRule::Type::Keyframes; }
+
+}

+ 13 - 0
Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.idl

@@ -0,0 +1,13 @@
+#import <CSS/CSSRule.idl>
+
+[Exposed=Window]
+interface CSSKeyframesRule : CSSRule {
+    attribute CSSOMString name;
+    readonly attribute CSSRuleList cssRules;
+    readonly attribute unsigned long length;
+
+    getter CSSKeyframeRule (unsigned long index);
+    undefined        appendRule(CSSOMString rule);
+    undefined        deleteRule(CSSOMString select);
+    CSSKeyframeRule? findRule(CSSOMString select);
+};

+ 2 - 0
Userland/Libraries/LibWeb/CSS/CSSRule.h

@@ -27,6 +27,8 @@ public:
         Import = 3,
         Import = 3,
         Media = 4,
         Media = 4,
         FontFace = 5,
         FontFace = 5,
+        Keyframes = 7,
+        Keyframe = 8,
         Supports = 12,
         Supports = 12,
     };
     };
 
 

+ 2 - 0
Userland/Libraries/LibWeb/CSS/CSSRule.idl

@@ -16,6 +16,8 @@ interface CSSRule {
     const unsigned short MEDIA_RULE = 4;
     const unsigned short MEDIA_RULE = 4;
     const unsigned short FONT_FACE_RULE = 5;
     const unsigned short FONT_FACE_RULE = 5;
     const unsigned short PAGE_RULE = 6;
     const unsigned short PAGE_RULE = 6;
+    const unsigned short KEYFRAMES_RULE = 7;
+    const unsigned short KEYFRAME_RULE = 8;
     const unsigned short MARGIN_RULE = 9;
     const unsigned short MARGIN_RULE = 9;
     const unsigned short NAMESPACE_RULE = 10;
     const unsigned short NAMESPACE_RULE = 10;
     const unsigned short SUPPORTS_RULE = 12;
     const unsigned short SUPPORTS_RULE = 12;

+ 36 - 0
Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp

@@ -8,6 +8,7 @@
 #include <LibWeb/Bindings/CSSRuleListPrototype.h>
 #include <LibWeb/Bindings/CSSRuleListPrototype.h>
 #include <LibWeb/Bindings/Intrinsics.h>
 #include <LibWeb/Bindings/Intrinsics.h>
 #include <LibWeb/CSS/CSSImportRule.h>
 #include <LibWeb/CSS/CSSImportRule.h>
+#include <LibWeb/CSS/CSSKeyframesRule.h>
 #include <LibWeb/CSS/CSSMediaRule.h>
 #include <LibWeb/CSS/CSSMediaRule.h>
 #include <LibWeb/CSS/CSSRule.h>
 #include <LibWeb/CSS/CSSRule.h>
 #include <LibWeb/CSS/CSSRuleList.h>
 #include <LibWeb/CSS/CSSRuleList.h>
@@ -141,6 +142,38 @@ void CSSRuleList::for_each_effective_style_rule(Function<void(CSSStyleRule const
         case CSSRule::Type::Supports:
         case CSSRule::Type::Supports:
             static_cast<CSSSupportsRule const&>(*rule).for_each_effective_style_rule(callback);
             static_cast<CSSSupportsRule const&>(*rule).for_each_effective_style_rule(callback);
             break;
             break;
+        case CSSRule::Type::Keyframe:
+        case CSSRule::Type::Keyframes:
+            break;
+        }
+    }
+}
+
+void CSSRuleList::for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const
+{
+    for (auto const& rule : m_rules) {
+        switch (rule->type()) {
+        case CSSRule::Type::FontFace:
+            break;
+        case CSSRule::Type::Import: {
+            auto const& import_rule = static_cast<CSSImportRule const&>(*rule);
+            if (import_rule.loaded_style_sheet())
+                import_rule.loaded_style_sheet()->for_each_effective_keyframes_at_rule(callback);
+            break;
+        }
+        case CSSRule::Type::Media:
+            static_cast<CSSMediaRule const&>(*rule).for_each_effective_keyframes_at_rule(callback);
+            break;
+        case CSSRule::Type::Style:
+            break;
+        case CSSRule::Type::Supports:
+            static_cast<CSSSupportsRule const&>(*rule).for_each_effective_keyframes_at_rule(callback);
+            break;
+        case CSSRule::Type::Keyframe:
+            break;
+        case CSSRule::Type::Keyframes:
+            callback(static_cast<CSSKeyframesRule const&>(*rule));
+            break;
         }
         }
     }
     }
 }
 }
@@ -177,6 +210,9 @@ bool CSSRuleList::evaluate_media_queries(HTML::Window const& window)
                 any_media_queries_changed_match_state = true;
                 any_media_queries_changed_match_state = true;
             break;
             break;
         }
         }
+        case CSSRule::Type::Keyframe:
+        case CSSRule::Type::Keyframes:
+            break;
         }
         }
     }
     }
 
 

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

@@ -59,6 +59,7 @@ public:
     void for_each_effective_style_rule(Function<void(CSSStyleRule const&)> const& callback) const;
     void for_each_effective_style_rule(Function<void(CSSStyleRule const&)> const& callback) const;
     // Returns whether the match state of any media queries changed after evaluation.
     // Returns whether the match state of any media queries changed after evaluation.
     bool evaluate_media_queries(HTML::Window const&);
     bool evaluate_media_queries(HTML::Window const&);
+    void for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const;
 
 
 private:
 private:
     explicit CSSRuleList(JS::Realm&);
     explicit CSSRuleList(JS::Realm&);

+ 2 - 1
Userland/Libraries/LibWeb/CSS/CSSStyleDeclaration.h

@@ -53,7 +53,8 @@ class PropertyOwningCSSStyleDeclaration : public CSSStyleDeclaration {
     friend class ElementInlineCSSStyleDeclaration;
     friend class ElementInlineCSSStyleDeclaration;
 
 
 public:
 public:
-    static WebIDL::ExceptionOr<JS::NonnullGCPtr<PropertyOwningCSSStyleDeclaration>> create(JS::Realm&, Vector<StyleProperty>, HashMap<DeprecatedString, StyleProperty> custom_properties);
+    static WebIDL::ExceptionOr<JS::NonnullGCPtr<PropertyOwningCSSStyleDeclaration>>
+    create(JS::Realm&, Vector<StyleProperty>, HashMap<DeprecatedString, StyleProperty> custom_properties);
 
 
     virtual ~PropertyOwningCSSStyleDeclaration() override = default;
     virtual ~PropertyOwningCSSStyleDeclaration() override = default;
 
 

+ 6 - 0
Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp

@@ -112,6 +112,12 @@ void CSSStyleSheet::for_each_effective_style_rule(Function<void(CSSStyleRule con
     }
     }
 }
 }
 
 
+void CSSStyleSheet::for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const
+{
+    if (m_media->matches())
+        m_rules->for_each_effective_keyframes_at_rule(callback);
+}
+
 bool CSSStyleSheet::evaluate_media_queries(HTML::Window const& window)
 bool CSSStyleSheet::evaluate_media_queries(HTML::Window const& window)
 {
 {
     bool any_media_queries_changed_match_state = false;
     bool any_media_queries_changed_match_state = false;

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

@@ -45,6 +45,7 @@ public:
     void for_each_effective_style_rule(Function<void(CSSStyleRule const&)> const& callback) const;
     void for_each_effective_style_rule(Function<void(CSSStyleRule const&)> const& callback) const;
     // Returns whether the match state of any media queries changed after evaluation.
     // Returns whether the match state of any media queries changed after evaluation.
     bool evaluate_media_queries(HTML::Window const&);
     bool evaluate_media_queries(HTML::Window const&);
+    void for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const;
 
 
     void set_style_sheet_list(Badge<StyleSheetList>, StyleSheetList*);
     void set_style_sheet_list(Badge<StyleSheetList>, StyleSheetList*);
 
 

+ 12 - 0
Userland/Libraries/LibWeb/CSS/Enums.json

@@ -32,6 +32,18 @@
         "stretch",
         "stretch",
         "unsafe"
         "unsafe"
     ],
     ],
+    "animation-fill-mode": [
+        "backwards",
+        "both",
+        "forwards",
+        "none"
+    ],
+    "animation-direction": [
+        "alternate",
+        "alternate-reverse",
+        "normal",
+        "reverse"
+    ],
     "appearance": [
     "appearance": [
         "auto",
         "auto",
         "button",
         "button",

+ 12 - 0
Userland/Libraries/LibWeb/CSS/Identifiers.json

@@ -61,9 +61,12 @@
   "alias",
   "alias",
   "all",
   "all",
   "all-scroll",
   "all-scroll",
+  "alternate",
+  "alternate-reverse",
   "anywhere",
   "anywhere",
   "auto",
   "auto",
   "back",
   "back",
+  "backwards",
   "baseline",
   "baseline",
   "blink",
   "blink",
   "block",
   "block",
@@ -109,6 +112,10 @@
   "dotted",
   "dotted",
   "double",
   "double",
   "e-resize",
   "e-resize",
+  "ease",
+  "ease-in",
+  "ease-in-out",
+  "ease-out",
   "enabled",
   "enabled",
   "end",
   "end",
   "ew-resize",
   "ew-resize",
@@ -126,6 +133,7 @@
   "flow",
   "flow",
   "flow-root",
   "flow-root",
   "from-font",
   "from-font",
+  "forwards",
   "full-size-kana",
   "full-size-kana",
   "full-width",
   "full-width",
   "fullscreen",
   "fullscreen",
@@ -161,6 +169,7 @@
   "less",
   "less",
   "light",
   "light",
   "lighter",
   "lighter",
+  "linear",
   "line-through",
   "line-through",
   "list-item",
   "list-item",
   "local",
   "local",
@@ -204,6 +213,7 @@
   "p3",
   "p3",
   "padding-box",
   "padding-box",
   "paged",
   "paged",
+  "paused",
   "pixelated",
   "pixelated",
   "pointer",
   "pointer",
   "portrait",
   "portrait",
@@ -220,6 +230,7 @@
   "repeat",
   "repeat",
   "repeat-x",
   "repeat-x",
   "repeat-y",
   "repeat-y",
+  "reverse",
   "ridge",
   "ridge",
   "right",
   "right",
   "round",
   "round",
@@ -232,6 +243,7 @@
   "ruby-base-container",
   "ruby-base-container",
   "ruby-text",
   "ruby-text",
   "ruby-text-container",
   "ruby-text-container",
+  "running",
   "run-in",
   "run-in",
   "radio",
   "radio",
   "s-resize",
   "s-resize",

+ 104 - 0
Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp

@@ -15,6 +15,8 @@
 #include <LibWeb/Bindings/MainThreadVM.h>
 #include <LibWeb/Bindings/MainThreadVM.h>
 #include <LibWeb/CSS/CSSFontFaceRule.h>
 #include <LibWeb/CSS/CSSFontFaceRule.h>
 #include <LibWeb/CSS/CSSImportRule.h>
 #include <LibWeb/CSS/CSSImportRule.h>
+#include <LibWeb/CSS/CSSKeyframeRule.h>
+#include <LibWeb/CSS/CSSKeyframesRule.h>
 #include <LibWeb/CSS/CSSMediaRule.h>
 #include <LibWeb/CSS/CSSMediaRule.h>
 #include <LibWeb/CSS/CSSStyleDeclaration.h>
 #include <LibWeb/CSS/CSSStyleDeclaration.h>
 #include <LibWeb/CSS/CSSStyleRule.h>
 #include <LibWeb/CSS/CSSStyleRule.h>
@@ -3164,6 +3166,108 @@ CSSRule* Parser::convert_to_rule(NonnullRefPtr<Rule> rule)
             auto rule_list = CSSRuleList::create(m_context.realm(), child_rules).release_value_but_fixme_should_propagate_errors();
             auto rule_list = CSSRuleList::create(m_context.realm(), child_rules).release_value_but_fixme_should_propagate_errors();
             return CSSSupportsRule::create(m_context.realm(), supports.release_nonnull(), rule_list).release_value_but_fixme_should_propagate_errors();
             return CSSSupportsRule::create(m_context.realm(), supports.release_nonnull(), rule_list).release_value_but_fixme_should_propagate_errors();
         }
         }
+        if (rule->at_rule_name().equals_ignoring_ascii_case("keyframes"sv)) {
+            auto prelude_stream = TokenStream { rule->prelude() };
+            prelude_stream.skip_whitespace();
+            auto token = prelude_stream.next_token();
+            if (!token.is_token()) {
+                dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule->prelude());
+                return {};
+            }
+
+            auto name_token = token.token();
+            prelude_stream.skip_whitespace();
+
+            if (prelude_stream.has_next_token()) {
+                dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes has invalid prelude, prelude = {}; discarding.", rule->prelude());
+                return {};
+            }
+
+            if (name_token.is(Token::Type::Ident) && (is_builtin(name_token.ident()) || name_token.ident().equals_ignoring_ascii_case("none"sv))) {
+                dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.ident());
+                return {};
+            }
+
+            if (!name_token.is(Token::Type::String) && !name_token.is(Token::Type::Ident)) {
+                dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule name is invalid: {}; discarding.", name_token.to_debug_string());
+                return {};
+            }
+
+            auto name = name_token.to_string().release_value_but_fixme_should_propagate_errors();
+
+            if (!rule->block())
+                return {};
+
+            auto child_tokens = TokenStream { rule->block()->values() };
+
+            Vector<JS::NonnullGCPtr<CSSKeyframeRule>> keyframes;
+            while (child_tokens.has_next_token()) {
+                child_tokens.skip_whitespace();
+                // keyframe-selector = <keyframe-keyword> | <percentage>
+                // keyframe-keyword = "from" | "to"
+                // selector = <keyframe-selector>#
+                // keyframes-block = "{" <declaration-list>? "}"
+                // keyframe-rule = <selector> <keyframes-block>
+
+                auto selectors = Vector<CSS::Percentage> {};
+                while (child_tokens.has_next_token()) {
+                    child_tokens.skip_whitespace();
+                    if (!child_tokens.has_next_token())
+                        break;
+                    auto tok = child_tokens.next_token();
+                    if (!tok.is_token()) {
+                        dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule has invalid selector: {}; discarding.", tok.to_debug_string());
+                        child_tokens.reconsume_current_input_token();
+                        break;
+                    }
+                    auto token = tok.token();
+                    auto read_a_selector = false;
+                    if (token.is(Token::Type::Ident)) {
+                        if (token.ident().equals_ignoring_ascii_case("from"sv)) {
+                            selectors.append(CSS::Percentage(0));
+                            read_a_selector = true;
+                        }
+                        if (token.ident().equals_ignoring_ascii_case("to"sv)) {
+                            selectors.append(CSS::Percentage(100));
+                            read_a_selector = true;
+                        }
+                    } else if (token.is(Token::Type::Percentage)) {
+                        selectors.append(CSS::Percentage(token.percentage()));
+                        read_a_selector = true;
+                    }
+
+                    if (read_a_selector) {
+                        child_tokens.skip_whitespace();
+                        if (child_tokens.next_token().is(Token::Type::Comma))
+                            continue;
+                    }
+
+                    child_tokens.reconsume_current_input_token();
+                    break;
+                }
+
+                if (!child_tokens.has_next_token())
+                    break;
+
+                child_tokens.skip_whitespace();
+                auto token = child_tokens.next_token();
+                if (token.is_block()) {
+                    auto block_tokens = token.block().values();
+                    auto block_stream = TokenStream { block_tokens };
+
+                    auto block_declarations = parse_a_list_of_declarations(block_stream);
+                    auto style = convert_to_style_declaration(block_declarations);
+                    for (auto& selector : selectors) {
+                        auto keyframe_rule = CSSKeyframeRule::create(m_context.realm(), selector, *style).release_value_but_fixme_should_propagate_errors();
+                        keyframes.append(keyframe_rule);
+                    }
+                } else {
+                    dbgln_if(CSS_PARSER_DEBUG, "CSSParser: @keyframes rule has invalid block: {}; discarding.", token.to_debug_string());
+                }
+            }
+
+            return CSSKeyframesRule::create(m_context.realm(), name, move(keyframes)).release_value_but_fixme_should_propagate_errors();
+        }
 
 
         // FIXME: More at rules!
         // FIXME: More at rules!
         dbgln_if(CSS_PARSER_DEBUG, "Unrecognized CSS at-rule: @{}", rule->at_rule_name());
         dbgln_if(CSS_PARSER_DEBUG, "Unrecognized CSS at-rule: @{}", rule->at_rule_name());

+ 9 - 2
Userland/Libraries/LibWeb/CSS/Parser/TokenStream.h

@@ -54,11 +54,18 @@ public:
         bool m_commit { false };
         bool m_commit { false };
     };
     };
 
 
-    explicit TokenStream(Vector<T> const& tokens)
+    explicit TokenStream(Span<T const> tokens)
         : m_tokens(tokens)
         : m_tokens(tokens)
         , m_eof(make_eof())
         , m_eof(make_eof())
     {
     {
     }
     }
+
+    explicit TokenStream(Vector<T> const& tokens)
+        : m_tokens(tokens.span())
+        , m_eof(make_eof())
+    {
+    }
+
     TokenStream(TokenStream<T> const&) = delete;
     TokenStream(TokenStream<T> const&) = delete;
     TokenStream(TokenStream<T>&&) = default;
     TokenStream(TokenStream<T>&&) = default;
 
 
@@ -128,7 +135,7 @@ public:
     }
     }
 
 
 private:
 private:
-    Vector<T> const& m_tokens;
+    Span<T const> m_tokens;
     int m_iterator_offset { -1 };
     int m_iterator_offset { -1 };
 
 
     T make_eof()
     T make_eof()

+ 98 - 1
Userland/Libraries/LibWeb/CSS/Properties.json

@@ -30,6 +30,103 @@
       "align-self"
       "align-self"
     ]
     ]
   },
   },
+  "animation": {
+    "affects-layout": true,
+    "inherited": false,
+    "initial": "none 0s ease 1 normal running 0s none",
+    "longhands": [
+      "animation-name",
+      "animation-duration",
+      "animation-timing-function",
+      "animation-iteration-count",
+      "animation-direction",
+      "animation-play-state",
+      "animation-delay",
+      "animation-fill-mode"
+    ]
+  },
+  "animation-name": {
+    "affects-layout": true,
+    "inherited": false,
+    "initial": "none",
+    "valid-types": [
+      "string", "custom-ident"
+    ],
+    "valid-identifiers": [
+      "none"
+    ]
+  },
+  "animation-duration": {
+    "affects-layout": true,
+    "inherited": false,
+    "initial": "0s",
+    "valid-types": [
+      "time [0,∞]"
+    ]
+  },
+  "animation-timing-function": {
+    "affects-layout": true,
+    "inherited": false,
+    "initial": "ease",
+    "__comment": "FIXME: This is like...wrong.",
+    "valid-identifiers": [
+      "ease",
+      "linear",
+      "ease-in-out",
+      "ease-in",
+      "ease-out"
+    ]
+  },
+  "animation-iteration-count": {
+    "affects-layout": true,
+    "inherited": false,
+    "initial": "1",
+    "valid-types": [
+      "number [0,∞]"
+    ],
+    "valid-identifiers": [
+      "infinite"
+    ]
+  },
+  "animation-direction": {
+    "affects-layout": false,
+    "inherited": false,
+    "initial": "normal",
+    "valid-identifiers": [
+      "normal",
+      "reverse",
+      "alternate",
+      "alternate-reverse"
+    ]
+  },
+  "animation-play-state": {
+    "affects-layout": false,
+    "inherited": false,
+    "initial": "running",
+    "valid-identifiers": [
+      "running",
+      "paused"
+    ]
+  },
+  "animation-delay": {
+    "affects-layout": true,
+    "inherited": false,
+    "initial": "0s",
+    "valid-types": [
+      "time"
+    ]
+  },
+  "animation-fill-mode": {
+    "affects-layout": true,
+    "inherited": false,
+    "initial": "none",
+    "valid-identifiers": [
+      "none",
+      "forwards",
+      "backwards",
+      "both"
+    ]
+  },
   "appearance": {
   "appearance": {
     "inherited": false,
     "inherited": false,
     "initial": "auto",
     "initial": "auto",
@@ -237,7 +334,7 @@
     "affects-layout": false,
     "affects-layout": false,
     "initial": "currentcolor",
     "initial": "currentcolor",
     "inherited": false,
     "inherited": false,
-    "valid-types":  [
+    "valid-types": [
       "color"
       "color"
     ],
     ],
     "quirks": [
     "quirks": [

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

@@ -986,6 +986,297 @@ static ErrorOr<void> cascade_custom_properties(DOM::Element& element, Optional<C
     return {};
     return {};
 }
 }
 
 
+StyleComputer::AnimationStepTransition StyleComputer::Animation::step(CSS::Time const& time_step)
+{
+    auto delay_ms = remaining_delay.to_milliseconds();
+    auto time_step_ms = time_step.to_milliseconds();
+
+    if (delay_ms > time_step_ms) {
+        remaining_delay = CSS::Time { static_cast<float>(delay_ms - time_step_ms), CSS::Time::Type::Ms };
+        return AnimationStepTransition::NoTransition;
+    }
+
+    remaining_delay = CSS::Time { 0, CSS::Time::Type::Ms };
+    time_step_ms -= delay_ms;
+
+    float added_progress = static_cast<float>(time_step_ms / duration.to_milliseconds());
+    auto new_progress = progress.as_fraction() + added_progress;
+    auto changed_iteration = false;
+    if (new_progress >= 1) {
+        if (iteration_count.has_value()) {
+            if (iteration_count.value() == 0) {
+                progress = CSS::Percentage(100);
+                return AnimationStepTransition::ActiveToAfter;
+            }
+            --iteration_count.value();
+            changed_iteration = true;
+        }
+        new_progress = 0;
+    }
+    progress = CSS::Percentage(new_progress * 100);
+
+    if (changed_iteration)
+        return AnimationStepTransition::ActiveToActiveChangingTheIteration;
+
+    return AnimationStepTransition::AfterToActive;
+}
+
+static ErrorOr<NonnullRefPtr<StyleValue>> interpolate_property(StyleValue const& from, StyleValue const& to, float delta)
+{
+    if (from.type() != to.type()) {
+        if (delta > 0.999f)
+            return to;
+        return from;
+    }
+
+    auto interpolate_raw = [delta = static_cast<double>(delta)](auto from, auto to) {
+        return static_cast<RemoveCVReference<decltype(from)>>(static_cast<double>(from) + static_cast<double>(to - from) * delta);
+    };
+
+    switch (from.type()) {
+    case StyleValue::Type::Angle:
+        return AngleStyleValue::create(Angle::make_degrees(interpolate_raw(from.as_angle().angle().to_degrees(), to.as_angle().angle().to_degrees())));
+    case StyleValue::Type::Color: {
+        auto from_color = from.as_color().color();
+        auto to_color = to.as_color().color();
+        auto from_hsv = from_color.to_hsv();
+        auto to_hsv = to_color.to_hsv();
+
+        auto color = Color::from_hsv(
+            interpolate_raw(from_hsv.hue, to_hsv.hue),
+            interpolate_raw(from_hsv.saturation, to_hsv.saturation),
+            interpolate_raw(from_hsv.value, to_hsv.value));
+        color.set_alpha(interpolate_raw(from_color.alpha(), to_color.alpha()));
+
+        return ColorStyleValue::create(color);
+    }
+    case StyleValue::Type::Length: {
+        auto& from_length = from.as_length().length();
+        auto& to_length = to.as_length().length();
+        return LengthStyleValue::create(Length(interpolate_raw(from_length.raw_value(), to_length.raw_value()), from_length.type()));
+    }
+    case StyleValue::Type::Numeric:
+        return NumericStyleValue::create_float(interpolate_raw(from.as_numeric().number(), to.as_numeric().number()));
+    case StyleValue::Type::Percentage:
+        return PercentageStyleValue::create(Percentage(interpolate_raw(from.as_percentage().percentage().value(), to.as_percentage().percentage().value())));
+    case StyleValue::Type::Position: {
+        auto& from_position = from.as_position();
+        auto& to_position = to.as_position();
+        return PositionStyleValue::create(
+            TRY(interpolate_property(from_position.edge_x(), to_position.edge_x(), delta)),
+            TRY(interpolate_property(from_position.edge_y(), to_position.edge_y(), delta)));
+    }
+    case StyleValue::Type::Rect: {
+        auto from_rect = from.as_rect().rect();
+        auto to_rect = to.as_rect().rect();
+        return RectStyleValue::create({
+            Length(interpolate_raw(from_rect.top_edge.raw_value(), to_rect.top_edge.raw_value()), from_rect.top_edge.type()),
+            Length(interpolate_raw(from_rect.right_edge.raw_value(), to_rect.right_edge.raw_value()), from_rect.right_edge.type()),
+            Length(interpolate_raw(from_rect.bottom_edge.raw_value(), to_rect.bottom_edge.raw_value()), from_rect.bottom_edge.type()),
+            Length(interpolate_raw(from_rect.left_edge.raw_value(), to_rect.left_edge.raw_value()), from_rect.left_edge.type()),
+        });
+    }
+    case StyleValue::Type::Transformation: {
+        auto& from_transform = from.as_transformation();
+        auto& to_transform = to.as_transformation();
+        if (from_transform.transform_function() != to_transform.transform_function())
+            return from;
+
+        auto from_input_values = from_transform.values();
+        auto to_input_values = to_transform.values();
+        if (from_input_values.size() != to_input_values.size())
+            return from;
+
+        StyleValueVector interpolated_values;
+        interpolated_values.ensure_capacity(from_input_values.size());
+        for (size_t i = 0; i < from_input_values.size(); ++i)
+            interpolated_values.append(TRY(interpolate_property(*from_input_values[i], *to_input_values[i], delta)));
+
+        return TransformationStyleValue::create(from_transform.transform_function(), move(interpolated_values));
+    }
+    case StyleValue::Type::ValueList: {
+        auto& from_list = from.as_value_list();
+        auto& to_list = to.as_value_list();
+        if (from_list.size() != to_list.size())
+            return from;
+
+        StyleValueVector interpolated_values;
+        interpolated_values.ensure_capacity(from_list.size());
+        for (size_t i = 0; i < from_list.size(); ++i)
+            interpolated_values.append(TRY(interpolate_property(from_list.values()[i], to_list.values()[i], delta)));
+
+        return StyleValueList::create(move(interpolated_values), from_list.separator());
+    }
+    default:
+        return from;
+    }
+}
+
+ErrorOr<void> StyleComputer::Animation::collect_into(StyleProperties& style_properties, RuleCache const& rule_cache) const
+{
+    if (remaining_delay.to_milliseconds() != 0)
+        return {};
+
+    auto matching_keyframes = rule_cache.rules_by_animation_keyframes.get(name);
+    if (!matching_keyframes.has_value())
+        return {};
+
+    auto& keyframes = matching_keyframes.value()->keyframes_by_key;
+
+    auto key = static_cast<u64>(progress.value() * AnimationKeyFrameKeyScaleFactor);
+    auto matching_keyframe_it = keyframes.find_largest_not_above_iterator(key);
+    if (matching_keyframe_it.is_end()) {
+        if constexpr (LIBWEB_CSS_ANIMATION_DEBUG) {
+            dbgln("    Did not find any start keyframe for the current state ({}) :(", key);
+            dbgln("    (have {} keyframes)", keyframes.size());
+            for (auto it = keyframes.begin(); it != keyframes.end(); ++it)
+                dbgln("        - {}", it.key());
+        }
+        return {};
+    }
+
+    auto keyframe_start = matching_keyframe_it.key();
+    auto keyframe_values = *matching_keyframe_it;
+
+    auto keyframe_end_it = ++matching_keyframe_it;
+    if (keyframe_end_it.is_end()) {
+        if constexpr (LIBWEB_CSS_ANIMATION_DEBUG) {
+            dbgln("    Did not find any end keyframe for the current state ({}) :(", key);
+            dbgln("    (have {} keyframes)", keyframes.size());
+            for (auto it = keyframes.begin(); it != keyframes.end(); ++it)
+                dbgln("        - {}", it.key());
+        }
+        return {};
+    }
+
+    auto keyframe_end = keyframe_end_it.key();
+    auto keyframe_end_values = *keyframe_end_it;
+
+    auto progress_in_keyframe = (progress.value() * AnimationKeyFrameKeyScaleFactor - keyframe_start) / (keyframe_end - keyframe_start);
+
+    auto valid_properties = 0;
+    for (auto const& property : keyframe_values.resolved_properties) {
+        if (property.has<Empty>())
+            continue;
+        valid_properties++;
+    }
+
+    dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Animation {} contains {} properties to interpolate, progress = {}%", name, valid_properties, progress_in_keyframe * 100);
+
+    UnderlyingType<PropertyID> property_id_value = 0;
+    for (auto const& property : keyframe_values.resolved_properties) {
+        auto property_id = static_cast<PropertyID>(property_id_value++);
+        if (property.has<Empty>())
+            continue;
+
+        auto resolve_property = [&](auto& property) {
+            return property.visit(
+                [](Empty) -> RefPtr<StyleValue const> { VERIFY_NOT_REACHED(); },
+                [&](AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial) {
+                    if (auto value = initial_state[to_underlying(property_id)])
+                        return value;
+
+                    auto value = style_properties.maybe_null_property(property_id);
+                    initial_state[to_underlying(property_id)] = value;
+                    return value;
+                },
+                [&](RefPtr<StyleValue const> value) { return value; });
+        };
+
+        auto resolved_start_property = resolve_property(property);
+
+        auto const& end_property = keyframe_end_values.resolved_properties[to_underlying(property_id)];
+        if (end_property.has<Empty>()) {
+            if (resolved_start_property) {
+                style_properties.set_property(property_id, resolved_start_property.release_nonnull());
+                dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "No end property for property {}, using {}", string_from_property_id(property_id), resolved_start_property->to_string());
+            }
+            continue;
+        }
+
+        auto resolved_end_property = resolve_property(end_property);
+
+        if (!resolved_start_property || !resolved_end_property)
+            continue;
+
+        auto start = resolved_start_property.release_nonnull();
+        auto end = resolved_end_property.release_nonnull();
+
+        // FIXME: This should be a function of the animation-timing-function.
+        auto next_value = TRY(interpolate_property(*start, *end, progress_in_keyframe));
+        dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Interpolated value for property {} at {}: {} -> {} = {}", string_from_property_id(property_id), progress_in_keyframe, start->to_string(), end->to_string(), next_value->to_string());
+        style_properties.set_property(property_id, next_value);
+    }
+
+    return {};
+}
+
+bool StyleComputer::Animation::is_done() const
+{
+    return progress.as_fraction() >= 0.9999f && iteration_count.has_value() && iteration_count.value() == 0;
+}
+
+void StyleComputer::ensure_animation_timer() const
+{
+    constexpr static auto timer_delay_ms = 1000 / 60;
+    if (!m_animation_driver_timer) {
+        m_animation_driver_timer = Platform::Timer::create_repeating(timer_delay_ms, [this] {
+            HashTable<AnimationKey> animations_to_remove;
+            HashTable<DOM::Element*> owning_elements_to_invalidate;
+
+            for (auto& it : m_active_animations) {
+                if (!it.value->owning_element) {
+                    // The element disappeared since we last ran, just discard the animation.
+                    animations_to_remove.set(it.key);
+                    continue;
+                }
+
+                auto transition = it.value->step(CSS::Time { timer_delay_ms, CSS::Time::Type::Ms });
+                owning_elements_to_invalidate.set(it.value->owning_element);
+
+                switch (transition) {
+                case AnimationStepTransition::NoTransition:
+                    break;
+                case AnimationStepTransition::IdleOrBeforeToActive:
+                    // FIXME: Dispatch `animationstart`.
+                    break;
+                case AnimationStepTransition::IdleOrBeforeToAfter:
+                    // FIXME: Dispatch `animationstart` then `animationend`.
+                    break;
+                case AnimationStepTransition::ActiveToBefore:
+                    // FIXME: Dispatch `animationend`.
+                    break;
+                case AnimationStepTransition::ActiveToActiveChangingTheIteration:
+                    // FIXME: Dispatch `animationiteration`.
+                    break;
+                case AnimationStepTransition::ActiveToAfter:
+                    // FIXME: Dispatch `animationend`.
+                    break;
+                case AnimationStepTransition::AfterToActive:
+                    // FIXME: Dispatch `animationstart`.
+                    break;
+                case AnimationStepTransition::AfterToBefore:
+                    // FIXME: Dispatch `animationstart` then `animationend`.
+                    break;
+                case AnimationStepTransition::Cancelled:
+                    // FIXME: Dispatch `animationcancel`.
+                    break;
+                }
+                if (it.value->is_done())
+                    animations_to_remove.set(it.key);
+            }
+
+            for (auto key : animations_to_remove)
+                m_active_animations.remove(key);
+
+            for (auto* element : owning_elements_to_invalidate)
+                element->invalidate_style();
+        });
+    }
+
+    m_animation_driver_timer->start();
+}
+
 // https://www.w3.org/TR/css-cascade/#cascading
 // https://www.w3.org/TR/css-cascade/#cascading
 ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element& element, Optional<CSS::Selector::PseudoElement> pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const
 ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element& element, Optional<CSS::Selector::PseudoElement> pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const
 {
 {
@@ -1036,7 +1327,56 @@ ErrorOr<void> StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
     // Normal author declarations
     // Normal author declarations
     cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::No);
     cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::No);
 
 
-    // FIXME: Animation declarations [css-animations-1]
+    // Animation declarations [css-animations-2]
+    if (auto animation_name = style.maybe_null_property(PropertyID::AnimationName)) {
+        ensure_animation_timer();
+
+        if (auto source_declaration = style.property_source_declaration(PropertyID::AnimationName)) {
+            AnimationKey animation_key {
+                .source_declaration = source_declaration,
+                .element = &element,
+            };
+            if (auto name = TRY(animation_name->to_string()); !name.is_empty()) {
+                auto active_animation = m_active_animations.get(animation_key);
+                if (!active_animation.has_value()) {
+                    // New animation!
+                    CSS::Time duration { 0, CSS::Time::Type::S };
+                    if (auto duration_value = style.maybe_null_property(PropertyID::AnimationDuration); duration_value && duration_value->is_time())
+                        duration = duration_value->as_time().time();
+
+                    CSS::Time delay { 0, CSS::Time::Type::S };
+                    if (auto delay_value = style.maybe_null_property(PropertyID::AnimationDelay); delay_value && delay_value->is_time())
+                        delay = delay_value->as_time().time();
+
+                    Optional<size_t> iteration_count = 1;
+                    if (auto iteration_count_value = style.maybe_null_property(PropertyID::AnimationIterationCount); iteration_count_value) {
+                        if (iteration_count_value->is_identifier() && iteration_count_value->to_identifier() == ValueID::Infinite)
+                            iteration_count = {};
+                        else if (iteration_count_value->is_numeric())
+                            iteration_count = static_cast<size_t>(iteration_count_value->as_numeric().number());
+                    }
+
+                    auto animation = make<Animation>(Animation {
+                        .name = move(name),
+                        .duration = duration,
+                        .delay = delay,
+                        .iteration_count = iteration_count,
+                        .direction = Animation::Direction::Normal,
+                        .fill_mode = Animation::FillMode::None,
+                        .owning_element = TRY(element.try_make_weak_ptr<DOM::Element>()),
+                        .progress = CSS::Percentage(0),
+                        .remaining_delay = delay,
+                    });
+                    active_animation = animation;
+                    m_active_animations.set(animation_key, move(animation));
+                }
+
+                TRY((*active_animation)->collect_into(style, rule_cache_for_cascade_origin(CascadeOrigin::Author)));
+            } else {
+                m_active_animations.remove(animation_key);
+            }
+        }
+    }
 
 
     // Important author declarations
     // Important author declarations
     cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::Yes);
     cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::Yes);
@@ -1709,6 +2049,93 @@ NonnullOwnPtr<StyleComputer::RuleCache> StyleComputer::make_rule_cache_for_casca
             }
             }
             ++rule_index;
             ++rule_index;
         });
         });
+
+        sheet.for_each_effective_keyframes_at_rule([&](CSSKeyframesRule const& rule) {
+            auto keyframe_set = make<AnimationKeyFrameSet>();
+            AnimationKeyFrameSet::ResolvedKeyFrame resolved_keyframe;
+
+            // Forwards pass, resolve all the user-specified keyframe properties.
+            for (auto const& keyframe : rule.keyframes()) {
+                auto key = static_cast<u64>(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor);
+                auto keyframe_rule = keyframe->style();
+
+                if (!is<PropertyOwningCSSStyleDeclaration>(*keyframe_rule))
+                    continue;
+
+                auto current_keyframe = resolved_keyframe;
+                auto& keyframe_style = static_cast<PropertyOwningCSSStyleDeclaration const&>(*keyframe_rule);
+                for (auto& property : keyframe_style.properties())
+                    current_keyframe.resolved_properties[to_underlying(property.property_id)] = property.value;
+
+                resolved_keyframe = move(current_keyframe);
+                keyframe_set->keyframes_by_key.insert(key, resolved_keyframe);
+            }
+
+            // If there is no 'from' keyframe, make a synthetic one.
+            auto made_a_synthetic_from_keyframe = false;
+            if (!keyframe_set->keyframes_by_key.find(0)) {
+                keyframe_set->keyframes_by_key.insert(0, AnimationKeyFrameSet::ResolvedKeyFrame());
+                made_a_synthetic_from_keyframe = true;
+            }
+
+            // Backwards pass, resolve all the implied properties, go read <https://drafts.csswg.org/css-animations-2/#keyframe-processing> to see why.
+            auto first = true;
+            for (auto const& keyframe : rule.keyframes().in_reverse()) {
+                auto key = static_cast<u64>(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor);
+                auto keyframe_rule = keyframe->style();
+
+                if (!is<PropertyOwningCSSStyleDeclaration>(*keyframe_rule))
+                    continue;
+
+                // The last keyframe is already fully resolved.
+                if (first) {
+                    first = false;
+                    continue;
+                }
+
+                auto next_keyframe = resolved_keyframe;
+                auto& current_keyframes = *keyframe_set->keyframes_by_key.find(key);
+
+                for (auto it = next_keyframe.resolved_properties.begin(); !it.is_end(); ++it) {
+                    auto& current_property = current_keyframes.resolved_properties[it.index()];
+                    if (!current_property.has<Empty>() || it->has<Empty>())
+                        continue;
+
+                    if (key == 0)
+                        current_property = AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial();
+                    else
+                        current_property = *it;
+                }
+
+                resolved_keyframe = current_keyframes;
+            }
+
+            if (made_a_synthetic_from_keyframe && !first) {
+                auto next_keyframe = resolved_keyframe;
+                auto& current_keyframes = *keyframe_set->keyframes_by_key.find(0);
+
+                for (auto it = next_keyframe.resolved_properties.begin(); !it.is_end(); ++it) {
+                    auto& current_property = current_keyframes.resolved_properties[it.index()];
+                    if (!current_property.has<Empty>() || it->has<Empty>())
+                        continue;
+                    current_property = AnimationKeyFrameSet::ResolvedKeyFrame::UseInitial();
+                }
+
+                resolved_keyframe = current_keyframes;
+            }
+
+            if constexpr (LIBWEB_CSS_DEBUG) {
+                dbgln("Resolved keyframe set '{}' into {} keyframes:", rule.name(), keyframe_set->keyframes_by_key.size());
+                for (auto it = keyframe_set->keyframes_by_key.begin(); it != keyframe_set->keyframes_by_key.end(); ++it) {
+                    size_t props = 0;
+                    for (auto& entry : it->resolved_properties)
+                        props += !entry.has<Empty>();
+                    dbgln("    - keyframe {}: {} properties", it.key(), props);
+                }
+            }
+
+            rule_cache->rules_by_animation_keyframes.set(rule.name(), move(keyframe_set));
+        });
         ++style_sheet_index;
         ++style_sheet_index;
     });
     });
 
 

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

@@ -10,7 +10,9 @@
 #include <AK/HashMap.h>
 #include <AK/HashMap.h>
 #include <AK/Optional.h>
 #include <AK/Optional.h>
 #include <AK/OwnPtr.h>
 #include <AK/OwnPtr.h>
+#include <AK/RedBlackTree.h>
 #include <LibWeb/CSS/CSSFontFaceRule.h>
 #include <LibWeb/CSS/CSSFontFaceRule.h>
+#include <LibWeb/CSS/CSSKeyframesRule.h>
 #include <LibWeb/CSS/CSSStyleDeclaration.h>
 #include <LibWeb/CSS/CSSStyleDeclaration.h>
 #include <LibWeb/CSS/Parser/ComponentValue.h>
 #include <LibWeb/CSS/Parser/ComponentValue.h>
 #include <LibWeb/CSS/Parser/TokenStream.h>
 #include <LibWeb/CSS/Parser/TokenStream.h>
@@ -88,6 +90,11 @@ public:
 
 
     void load_fonts_from_sheet(CSSStyleSheet const&);
     void load_fonts_from_sheet(CSSStyleSheet const&);
 
 
+    struct AnimationKey {
+        CSS::CSSStyleDeclaration const* source_declaration;
+        DOM::Element const* element;
+    };
+
 private:
 private:
     enum class ComputeStyleMode {
     enum class ComputeStyleMode {
         Normal,
         Normal,
@@ -126,17 +133,29 @@ private:
 
 
     JS::NonnullGCPtr<DOM::Document> m_document;
     JS::NonnullGCPtr<DOM::Document> m_document;
 
 
+    struct AnimationKeyFrameSet {
+        struct ResolvedKeyFrame {
+            struct UseInitial { };
+            Array<Variant<Empty, UseInitial, NonnullRefPtr<StyleValue const>>, to_underlying(last_property_id) + 1> resolved_properties {};
+        };
+        RedBlackTree<u64, ResolvedKeyFrame> keyframes_by_key;
+    };
+
     struct RuleCache {
     struct RuleCache {
         HashMap<FlyString, Vector<MatchingRule>> rules_by_id;
         HashMap<FlyString, Vector<MatchingRule>> rules_by_id;
         HashMap<FlyString, Vector<MatchingRule>> rules_by_class;
         HashMap<FlyString, Vector<MatchingRule>> rules_by_class;
         HashMap<FlyString, Vector<MatchingRule>> rules_by_tag_name;
         HashMap<FlyString, Vector<MatchingRule>> rules_by_tag_name;
         Vector<MatchingRule> other_rules;
         Vector<MatchingRule> other_rules;
+
+        HashMap<FlyString, NonnullOwnPtr<AnimationKeyFrameSet>> rules_by_animation_keyframes;
     };
     };
 
 
     NonnullOwnPtr<RuleCache> make_rule_cache_for_cascade_origin(CascadeOrigin);
     NonnullOwnPtr<RuleCache> make_rule_cache_for_cascade_origin(CascadeOrigin);
 
 
     RuleCache const& rule_cache_for_cascade_origin(CascadeOrigin) const;
     RuleCache const& rule_cache_for_cascade_origin(CascadeOrigin) const;
 
 
+    void ensure_animation_timer() const;
+
     OwnPtr<RuleCache> m_author_rule_cache;
     OwnPtr<RuleCache> m_author_rule_cache;
     OwnPtr<RuleCache> m_user_agent_rule_cache;
     OwnPtr<RuleCache> m_user_agent_rule_cache;
 
 
@@ -145,6 +164,57 @@ private:
 
 
     Length::FontMetrics m_default_font_metrics;
     Length::FontMetrics m_default_font_metrics;
     Length::FontMetrics m_root_element_font_metrics;
     Length::FontMetrics m_root_element_font_metrics;
+
+    constexpr static u64 AnimationKeyFrameKeyScaleFactor = 1000; // 0..100000
+
+    enum class AnimationStepTransition {
+        NoTransition,
+        IdleOrBeforeToActive,
+        IdleOrBeforeToAfter,
+        ActiveToBefore,
+        ActiveToActiveChangingTheIteration,
+        ActiveToAfter,
+        AfterToActive,
+        AfterToBefore,
+        Cancelled,
+    };
+    enum class AnimationState {
+        Before,
+        After,
+        Idle,
+        Active,
+    };
+
+    struct Animation {
+        String name;
+        CSS::Time duration;
+        CSS::Time delay;
+        Optional<size_t> iteration_count; // Infinite if not set.
+        CSS::AnimationDirection direction;
+        CSS::AnimationFillMode fill_mode;
+        WeakPtr<DOM::Element> owning_element;
+
+        CSS::Percentage progress { 0 };
+        CSS::Time remaining_delay { 0, CSS::Time::Type::Ms };
+        AnimationState current_state { AnimationState::Before };
+        mutable Array<RefPtr<StyleValue const>, to_underlying(last_property_id) + 1> initial_state {};
+
+        AnimationStepTransition step(CSS::Time const& time_step);
+        ErrorOr<void> collect_into(StyleProperties&, RuleCache const&) const;
+        bool is_done() const;
+    };
+
+    mutable HashMap<AnimationKey, NonnullOwnPtr<Animation>> m_active_animations;
+    mutable RefPtr<Platform::Timer> m_animation_driver_timer;
 };
 };
 
 
 }
 }
+
+template<>
+struct AK::Traits<Web::CSS::StyleComputer::AnimationKey> : public AK::GenericTraits<Web::CSS::StyleComputer::AnimationKey> {
+    static unsigned hash(Web::CSS::StyleComputer::AnimationKey const& k) { return pair_int_hash(ptr_hash(k.source_declaration), ptr_hash(k.element)); }
+    static bool equals(Web::CSS::StyleComputer::AnimationKey const& a, Web::CSS::StyleComputer::AnimationKey const& b)
+    {
+        return a.element == b.element && a.source_declaration == b.source_declaration;
+    }
+};

+ 3 - 0
Userland/Libraries/LibWeb/Dump.cpp

@@ -652,6 +652,9 @@ ErrorOr<void> dump_rule(StringBuilder& builder, CSS::CSSRule const& rule, int in
     case CSS::CSSRule::Type::Supports:
     case CSS::CSSRule::Type::Supports:
         TRY(dump_supports_rule(builder, verify_cast<CSS::CSSSupportsRule const>(rule), indent_levels));
         TRY(dump_supports_rule(builder, verify_cast<CSS::CSSSupportsRule const>(rule), indent_levels));
         break;
         break;
+    case CSS::CSSRule::Type::Keyframe:
+    case CSS::CSSRule::Type::Keyframes:
+        break;
     }
     }
     return {};
     return {};
 }
 }

+ 9 - 7
Userland/Libraries/LibWeb/Forward.h

@@ -68,16 +68,12 @@ class BackgroundStyleValue;
 class BorderRadiusShorthandStyleValue;
 class BorderRadiusShorthandStyleValue;
 class BorderRadiusStyleValue;
 class BorderRadiusStyleValue;
 class BorderStyleValue;
 class BorderStyleValue;
-class CalculatedStyleValue;
-class Clip;
-class ColorStyleValue;
-class CompositeStyleValue;
-class ConicGradientStyleValue;
-class ContentStyleValue;
 class CSSConditionRule;
 class CSSConditionRule;
 class CSSFontFaceRule;
 class CSSFontFaceRule;
 class CSSGroupingRule;
 class CSSGroupingRule;
 class CSSImportRule;
 class CSSImportRule;
+class CSSKeyframeRule;
+class CSSKeyframesRule;
 class CSSMediaRule;
 class CSSMediaRule;
 class CSSRule;
 class CSSRule;
 class CSSRuleList;
 class CSSRuleList;
@@ -85,6 +81,12 @@ class CSSStyleDeclaration;
 class CSSStyleRule;
 class CSSStyleRule;
 class CSSStyleSheet;
 class CSSStyleSheet;
 class CSSSupportsRule;
 class CSSSupportsRule;
+class CalculatedStyleValue;
+class Clip;
+class ColorStyleValue;
+class CompositeStyleValue;
+class ConicGradientStyleValue;
+class ContentStyleValue;
 class CustomIdentStyleValue;
 class CustomIdentStyleValue;
 class Display;
 class Display;
 class DisplayStyleValue;
 class DisplayStyleValue;
@@ -157,10 +159,10 @@ class TimeOrCalculated;
 class TimePercentage;
 class TimePercentage;
 class TimeStyleValue;
 class TimeStyleValue;
 class TransformationStyleValue;
 class TransformationStyleValue;
+class URLStyleValue;
 class UnicodeRange;
 class UnicodeRange;
 class UnresolvedStyleValue;
 class UnresolvedStyleValue;
 class UnsetStyleValue;
 class UnsetStyleValue;
-class URLStyleValue;
 
 
 enum class MediaFeatureID;
 enum class MediaFeatureID;
 enum class PropertyID;
 enum class PropertyID;