diff --git a/AK/Debug.h.in b/AK/Debug.h.in
index b81b9a0853..087aa34a35 100644
--- a/AK/Debug.h.in
+++ b/AK/Debug.h.in
@@ -242,6 +242,10 @@
# cmakedefine01 LIBWEB_CSS_DEBUG
#endif
+#ifndef LIBWEB_CSS_ANIMATION_DEBUG
+# cmakedefine01 LIBWEB_CSS_ANIMATION_DEBUG
+#endif
+
#ifndef LINE_EDITOR_DEBUG
# cmakedefine01 LINE_EDITOR_DEBUG
#endif
diff --git a/Base/res/html/misc/css-animations.html b/Base/res/html/misc/css-animations.html
new file mode 100644
index 0000000000..99862f96c1
--- /dev/null
+++ b/Base/res/html/misc/css-animations.html
@@ -0,0 +1,50 @@
+
+
+
diff --git a/Base/res/html/misc/welcome.html b/Base/res/html/misc/welcome.html
index 1ed1c42dbd..800828bb20 100644
--- a/Base/res/html/misc/welcome.html
+++ b/Base/res/html/misc/welcome.html
@@ -163,6 +163,7 @@
Styling "inline" elements
Pseudo-elements (::before, ::after, etc)
Effects with opacity and transforms
+ CSS Animations
JavaScript/Wasm
diff --git a/Meta/CMake/all_the_debug_macros.cmake b/Meta/CMake/all_the_debug_macros.cmake
index 6c5e6d7a5a..2a2b9a2611 100644
--- a/Meta/CMake/all_the_debug_macros.cmake
+++ b/Meta/CMake/all_the_debug_macros.cmake
@@ -92,6 +92,7 @@ set(KEYBOARD_SHORTCUTS_DEBUG ON)
set(KMALLOC_DEBUG ON)
set(LANGUAGE_SERVER_DEBUG ON)
set(LEXER_DEBUG ON)
+set(LIBWEB_CSS_ANIMATION_DEBUG ON)
set(LIBWEB_CSS_DEBUG ON)
set(LINE_EDITOR_DEBUG ON)
set(LOCAL_SOCKET_DEBUG ON)
diff --git a/Userland/Libraries/LibWeb/CMakeLists.txt b/Userland/Libraries/LibWeb/CMakeLists.txt
index a03cd83ffe..8d9e79f8e0 100644
--- a/Userland/Libraries/LibWeb/CMakeLists.txt
+++ b/Userland/Libraries/LibWeb/CMakeLists.txt
@@ -22,6 +22,8 @@ set(SOURCES
CSS/CSSConditionRule.cpp
CSS/CSSGroupingRule.cpp
CSS/CSSImportRule.cpp
+ CSS/CSSKeyframeRule.cpp
+ CSS/CSSKeyframesRule.cpp
CSS/CSSFontFaceRule.cpp
CSS/CSSMediaRule.cpp
CSS/CSSRule.cpp
diff --git a/Userland/Libraries/LibWeb/CSS/CSSConditionRule.cpp b/Userland/Libraries/LibWeb/CSS/CSSConditionRule.cpp
index 5e4be880de..172a23e78c 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSConditionRule.cpp
+++ b/Userland/Libraries/LibWeb/CSS/CSSConditionRule.cpp
@@ -22,6 +22,12 @@ void CSSConditionRule::for_each_effective_style_rule(Function const& callback) const
+{
+ if (condition_matches())
+ CSSGroupingRule::for_each_effective_keyframes_at_rule(callback);
+}
+
JS::ThrowCompletionOr CSSConditionRule::initialize(JS::Realm& realm)
{
MUST_OR_THROW_OOM(Base::initialize(realm));
diff --git a/Userland/Libraries/LibWeb/CSS/CSSConditionRule.h b/Userland/Libraries/LibWeb/CSS/CSSConditionRule.h
index 825106bb38..b705f47ac3 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSConditionRule.h
+++ b/Userland/Libraries/LibWeb/CSS/CSSConditionRule.h
@@ -23,6 +23,7 @@ public:
virtual bool condition_matches() const = 0;
virtual void for_each_effective_style_rule(Function const& callback) const override;
+ virtual void for_each_effective_keyframes_at_rule(Function const& callback) const override;
protected:
CSSConditionRule(JS::Realm&, CSSRuleList&);
diff --git a/Userland/Libraries/LibWeb/CSS/CSSGroupingRule.cpp b/Userland/Libraries/LibWeb/CSS/CSSGroupingRule.cpp
index 23958d2b78..b91aa011e0 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSGroupingRule.cpp
+++ b/Userland/Libraries/LibWeb/CSS/CSSGroupingRule.cpp
@@ -54,6 +54,11 @@ void CSSGroupingRule::for_each_effective_style_rule(Functionfor_each_effective_style_rule(callback);
}
+void CSSGroupingRule::for_each_effective_keyframes_at_rule(Function const& callback) const
+{
+ m_rules->for_each_effective_keyframes_at_rule(callback);
+}
+
void CSSGroupingRule::set_parent_style_sheet(CSSStyleSheet* parent_style_sheet)
{
CSSRule::set_parent_style_sheet(parent_style_sheet);
diff --git a/Userland/Libraries/LibWeb/CSS/CSSGroupingRule.h b/Userland/Libraries/LibWeb/CSS/CSSGroupingRule.h
index f39b20ab44..826cae8ee2 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSGroupingRule.h
+++ b/Userland/Libraries/LibWeb/CSS/CSSGroupingRule.h
@@ -27,6 +27,7 @@ public:
WebIDL::ExceptionOr delete_rule(u32 index);
virtual void for_each_effective_style_rule(Function const& callback) const;
+ virtual void for_each_effective_keyframes_at_rule(Function const& callback) const;
virtual void set_parent_style_sheet(CSSStyleSheet*) override;
diff --git a/Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.cpp b/Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.cpp
new file mode 100644
index 0000000000..35de9eaa74
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.cpp
@@ -0,0 +1,30 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include "CSSKeyframeRule.h"
+#include
+
+namespace Web::CSS {
+
+void CSSKeyframeRule::visit_edges(Visitor& visitor)
+{
+ Base::visit_edges(visitor);
+ visitor.visit(m_declarations);
+}
+
+JS::ThrowCompletionOr CSSKeyframeRule::initialize(JS::Realm&)
+{
+ return {};
+}
+
+DeprecatedString CSSKeyframeRule::serialized() const
+{
+ StringBuilder builder;
+ builder.appendff("{}% {{ {} }}", key().value(), style()->serialized());
+ return builder.to_deprecated_string();
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.h b/Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.h
new file mode 100644
index 0000000000..843f3a9aac
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+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> create(JS::Realm& realm, CSS::Percentage key, CSSStyleDeclaration& declarations)
+ {
+ return MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm, key, declarations));
+ }
+
+ virtual ~CSSKeyframeRule() = default;
+
+ virtual Type type() const override { return Type::Keyframe; };
+
+ CSS::Percentage key() const { return m_key; }
+ JS::NonnullGCPtr 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 initialize(JS::Realm&) override;
+ virtual DeprecatedString serialized() const override;
+
+ CSS::Percentage m_key;
+ JS::NonnullGCPtr m_declarations;
+};
+
+template<>
+inline bool CSSRule::fast_is() const { return type() == CSSRule::Type::Keyframe; }
+
+}
diff --git a/Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.idl b/Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.idl
new file mode 100644
index 0000000000..fd9c8de99f
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.idl
@@ -0,0 +1,7 @@
+#import
+
+[Exposed = Window]
+interface CSSKeyframeRule : CSSRule {
+ attribute CSSOMString keyText;
+ [SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
+};
diff --git a/Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.cpp b/Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.cpp
new file mode 100644
index 0000000000..33a061d6e3
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.cpp
@@ -0,0 +1,36 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur
+ *
+ * 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 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();
+}
+
+}
diff --git a/Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.h b/Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.h
new file mode 100644
index 0000000000..6c67f6cbc7
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2023, Ali Mohammad Pur
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+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> create(JS::Realm& realm, FlyString name, Vector> keyframes)
+ {
+ return MUST_OR_THROW_OOM(realm.heap().allocate(realm, realm, move(name), move(keyframes)));
+ }
+
+ virtual ~CSSKeyframesRule() = default;
+
+ virtual Type type() const override { return Type::Keyframes; };
+
+ Vector> const& keyframes() const { return m_keyframes; }
+ FlyString const& name() const { return m_name; }
+
+private:
+ CSSKeyframesRule(JS::Realm& realm, FlyString name, Vector> keyframes)
+ : CSSRule(realm)
+ , m_name(move(name))
+ , m_keyframes(move(keyframes))
+ {
+ }
+
+ virtual void visit_edges(Visitor&) override;
+
+ virtual JS::ThrowCompletionOr initialize(JS::Realm&) override;
+ virtual DeprecatedString serialized() const override;
+
+ FlyString m_name;
+ Vector> m_keyframes;
+};
+
+template<>
+inline bool CSSRule::fast_is() const { return type() == CSSRule::Type::Keyframes; }
+
+}
diff --git a/Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.idl b/Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.idl
new file mode 100644
index 0000000000..e307938403
--- /dev/null
+++ b/Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.idl
@@ -0,0 +1,13 @@
+#import
+
+[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);
+};
diff --git a/Userland/Libraries/LibWeb/CSS/CSSRule.h b/Userland/Libraries/LibWeb/CSS/CSSRule.h
index 61c229eb86..db9ae0bcb7 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSRule.h
+++ b/Userland/Libraries/LibWeb/CSS/CSSRule.h
@@ -27,6 +27,8 @@ public:
Import = 3,
Media = 4,
FontFace = 5,
+ Keyframes = 7,
+ Keyframe = 8,
Supports = 12,
};
diff --git a/Userland/Libraries/LibWeb/CSS/CSSRule.idl b/Userland/Libraries/LibWeb/CSS/CSSRule.idl
index bd40b41bd6..0c9d44327f 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSRule.idl
+++ b/Userland/Libraries/LibWeb/CSS/CSSRule.idl
@@ -16,6 +16,8 @@ interface CSSRule {
const unsigned short MEDIA_RULE = 4;
const unsigned short FONT_FACE_RULE = 5;
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 NAMESPACE_RULE = 10;
const unsigned short SUPPORTS_RULE = 12;
diff --git a/Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp b/Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp
index 27c3e5335f..60b9894a84 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp
+++ b/Userland/Libraries/LibWeb/CSS/CSSRuleList.cpp
@@ -8,6 +8,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -141,6 +142,38 @@ void CSSRuleList::for_each_effective_style_rule(Function(*rule).for_each_effective_style_rule(callback);
break;
+ case CSSRule::Type::Keyframe:
+ case CSSRule::Type::Keyframes:
+ break;
+ }
+ }
+}
+
+void CSSRuleList::for_each_effective_keyframes_at_rule(Function 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(*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(*rule).for_each_effective_keyframes_at_rule(callback);
+ break;
+ case CSSRule::Type::Style:
+ break;
+ case CSSRule::Type::Supports:
+ static_cast(*rule).for_each_effective_keyframes_at_rule(callback);
+ break;
+ case CSSRule::Type::Keyframe:
+ break;
+ case CSSRule::Type::Keyframes:
+ callback(static_cast(*rule));
+ break;
}
}
}
@@ -177,6 +210,9 @@ bool CSSRuleList::evaluate_media_queries(HTML::Window const& window)
any_media_queries_changed_match_state = true;
break;
}
+ case CSSRule::Type::Keyframe:
+ case CSSRule::Type::Keyframes:
+ break;
}
}
diff --git a/Userland/Libraries/LibWeb/CSS/CSSRuleList.h b/Userland/Libraries/LibWeb/CSS/CSSRuleList.h
index 7d9bd7149d..199adfc085 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSRuleList.h
+++ b/Userland/Libraries/LibWeb/CSS/CSSRuleList.h
@@ -59,6 +59,7 @@ public:
void for_each_effective_style_rule(Function const& callback) const;
// Returns whether the match state of any media queries changed after evaluation.
bool evaluate_media_queries(HTML::Window const&);
+ void for_each_effective_keyframes_at_rule(Function const& callback) const;
private:
explicit CSSRuleList(JS::Realm&);
diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleDeclaration.h b/Userland/Libraries/LibWeb/CSS/CSSStyleDeclaration.h
index c349dc4a20..139994eaf2 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSStyleDeclaration.h
+++ b/Userland/Libraries/LibWeb/CSS/CSSStyleDeclaration.h
@@ -53,7 +53,8 @@ class PropertyOwningCSSStyleDeclaration : public CSSStyleDeclaration {
friend class ElementInlineCSSStyleDeclaration;
public:
- static WebIDL::ExceptionOr> create(JS::Realm&, Vector, HashMap custom_properties);
+ static WebIDL::ExceptionOr>
+ create(JS::Realm&, Vector, HashMap custom_properties);
virtual ~PropertyOwningCSSStyleDeclaration() override = default;
diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp b/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp
index 8a05413f46..7c09a573aa 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp
+++ b/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.cpp
@@ -112,6 +112,12 @@ void CSSStyleSheet::for_each_effective_style_rule(Function 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 any_media_queries_changed_match_state = false;
diff --git a/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h b/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h
index 157c7ddefe..d9114ced0f 100644
--- a/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h
+++ b/Userland/Libraries/LibWeb/CSS/CSSStyleSheet.h
@@ -45,6 +45,7 @@ public:
void for_each_effective_style_rule(Function const& callback) const;
// Returns whether the match state of any media queries changed after evaluation.
bool evaluate_media_queries(HTML::Window const&);
+ void for_each_effective_keyframes_at_rule(Function const& callback) const;
void set_style_sheet_list(Badge, StyleSheetList*);
diff --git a/Userland/Libraries/LibWeb/CSS/Enums.json b/Userland/Libraries/LibWeb/CSS/Enums.json
index 34cdc7fb46..2859f31787 100644
--- a/Userland/Libraries/LibWeb/CSS/Enums.json
+++ b/Userland/Libraries/LibWeb/CSS/Enums.json
@@ -32,6 +32,18 @@
"stretch",
"unsafe"
],
+ "animation-fill-mode": [
+ "backwards",
+ "both",
+ "forwards",
+ "none"
+ ],
+ "animation-direction": [
+ "alternate",
+ "alternate-reverse",
+ "normal",
+ "reverse"
+ ],
"appearance": [
"auto",
"button",
diff --git a/Userland/Libraries/LibWeb/CSS/Identifiers.json b/Userland/Libraries/LibWeb/CSS/Identifiers.json
index 3110d40ee3..5a0fa0df50 100644
--- a/Userland/Libraries/LibWeb/CSS/Identifiers.json
+++ b/Userland/Libraries/LibWeb/CSS/Identifiers.json
@@ -61,9 +61,12 @@
"alias",
"all",
"all-scroll",
+ "alternate",
+ "alternate-reverse",
"anywhere",
"auto",
"back",
+ "backwards",
"baseline",
"blink",
"block",
@@ -109,6 +112,10 @@
"dotted",
"double",
"e-resize",
+ "ease",
+ "ease-in",
+ "ease-in-out",
+ "ease-out",
"enabled",
"end",
"ew-resize",
@@ -126,6 +133,7 @@
"flow",
"flow-root",
"from-font",
+ "forwards",
"full-size-kana",
"full-width",
"fullscreen",
@@ -161,6 +169,7 @@
"less",
"light",
"lighter",
+ "linear",
"line-through",
"list-item",
"local",
@@ -204,6 +213,7 @@
"p3",
"padding-box",
"paged",
+ "paused",
"pixelated",
"pointer",
"portrait",
@@ -220,6 +230,7 @@
"repeat",
"repeat-x",
"repeat-y",
+ "reverse",
"ridge",
"right",
"round",
@@ -232,6 +243,7 @@
"ruby-base-container",
"ruby-text",
"ruby-text-container",
+ "running",
"run-in",
"radio",
"s-resize",
diff --git a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
index 407fcfaff8..8c75b3b8d5 100644
--- a/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
+++ b/Userland/Libraries/LibWeb/CSS/Parser/Parser.cpp
@@ -15,6 +15,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -3164,6 +3166,108 @@ CSSRule* Parser::convert_to_rule(NonnullRefPtr rule)
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();
}
+ 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> keyframes;
+ while (child_tokens.has_next_token()) {
+ child_tokens.skip_whitespace();
+ // keyframe-selector = |
+ // keyframe-keyword = "from" | "to"
+ // selector = #
+ // keyframes-block = "{" ? "}"
+ // keyframe-rule =
+
+ auto selectors = Vector {};
+ 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!
dbgln_if(CSS_PARSER_DEBUG, "Unrecognized CSS at-rule: @{}", rule->at_rule_name());
diff --git a/Userland/Libraries/LibWeb/CSS/Parser/TokenStream.h b/Userland/Libraries/LibWeb/CSS/Parser/TokenStream.h
index 7cbc4f2e84..ea654e7137 100644
--- a/Userland/Libraries/LibWeb/CSS/Parser/TokenStream.h
+++ b/Userland/Libraries/LibWeb/CSS/Parser/TokenStream.h
@@ -54,11 +54,18 @@ public:
bool m_commit { false };
};
- explicit TokenStream(Vector const& tokens)
+ explicit TokenStream(Span tokens)
: m_tokens(tokens)
, m_eof(make_eof())
{
}
+
+ explicit TokenStream(Vector const& tokens)
+ : m_tokens(tokens.span())
+ , m_eof(make_eof())
+ {
+ }
+
TokenStream(TokenStream const&) = delete;
TokenStream(TokenStream&&) = default;
@@ -128,7 +135,7 @@ public:
}
private:
- Vector const& m_tokens;
+ Span m_tokens;
int m_iterator_offset { -1 };
T make_eof()
diff --git a/Userland/Libraries/LibWeb/CSS/Properties.json b/Userland/Libraries/LibWeb/CSS/Properties.json
index 38d884c8c0..42bd632c05 100644
--- a/Userland/Libraries/LibWeb/CSS/Properties.json
+++ b/Userland/Libraries/LibWeb/CSS/Properties.json
@@ -30,6 +30,103 @@
"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": {
"inherited": false,
"initial": "auto",
@@ -237,7 +334,7 @@
"affects-layout": false,
"initial": "currentcolor",
"inherited": false,
- "valid-types": [
+ "valid-types": [
"color"
],
"quirks": [
diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
index 86a104efdb..88b029a002 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
+++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.cpp
@@ -986,6 +986,297 @@ static ErrorOr cascade_custom_properties(DOM::Element& element, Optional time_step_ms) {
+ remaining_delay = CSS::Time { static_cast(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(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> 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(delta)](auto from, auto to) {
+ return static_cast>(static_cast(from) + static_cast(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 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(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())
+ continue;
+ valid_properties++;
+ }
+
+ dbgln_if(LIBWEB_CSS_ANIMATION_DEBUG, "Animation {} contains {} properties to interpolate, progress = {}%", name, valid_properties, progress_in_keyframe * 100);
+
+ UnderlyingType property_id_value = 0;
+ for (auto const& property : keyframe_values.resolved_properties) {
+ auto property_id = static_cast(property_id_value++);
+ if (property.has())
+ continue;
+
+ auto resolve_property = [&](auto& property) {
+ return property.visit(
+ [](Empty) -> RefPtr { 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 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()) {
+ 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 animations_to_remove;
+ HashTable 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
ErrorOr StyleComputer::compute_cascaded_values(StyleProperties& style, DOM::Element& element, Optional pseudo_element, bool& did_match_any_pseudo_element_rules, ComputeStyleMode mode) const
{
@@ -1036,7 +1327,56 @@ ErrorOr StyleComputer::compute_cascaded_values(StyleProperties& style, DOM
// Normal author declarations
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 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(iteration_count_value->as_numeric().number());
+ }
+
+ auto animation = make(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()),
+ .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
cascade_declarations(style, element, pseudo_element, matching_rule_set.author_rules, CascadeOrigin::Author, Important::Yes);
@@ -1709,6 +2049,93 @@ NonnullOwnPtr StyleComputer::make_rule_cache_for_casca
}
++rule_index;
});
+
+ sheet.for_each_effective_keyframes_at_rule([&](CSSKeyframesRule const& rule) {
+ auto keyframe_set = make();
+ AnimationKeyFrameSet::ResolvedKeyFrame resolved_keyframe;
+
+ // Forwards pass, resolve all the user-specified keyframe properties.
+ for (auto const& keyframe : rule.keyframes()) {
+ auto key = static_cast(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor);
+ auto keyframe_rule = keyframe->style();
+
+ if (!is(*keyframe_rule))
+ continue;
+
+ auto current_keyframe = resolved_keyframe;
+ auto& keyframe_style = static_cast(*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 to see why.
+ auto first = true;
+ for (auto const& keyframe : rule.keyframes().in_reverse()) {
+ auto key = static_cast(keyframe->key().value() * AnimationKeyFrameKeyScaleFactor);
+ auto keyframe_rule = keyframe->style();
+
+ if (!is(*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() || it->has())
+ 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() || it->has())
+ 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();
+ dbgln(" - keyframe {}: {} properties", it.key(), props);
+ }
+ }
+
+ rule_cache->rules_by_animation_keyframes.set(rule.name(), move(keyframe_set));
+ });
++style_sheet_index;
});
diff --git a/Userland/Libraries/LibWeb/CSS/StyleComputer.h b/Userland/Libraries/LibWeb/CSS/StyleComputer.h
index 83b997f94f..7c9fbf711e 100644
--- a/Userland/Libraries/LibWeb/CSS/StyleComputer.h
+++ b/Userland/Libraries/LibWeb/CSS/StyleComputer.h
@@ -10,7 +10,9 @@
#include
#include
#include
+#include
#include
+#include
#include
#include
#include
@@ -88,6 +90,11 @@ public:
void load_fonts_from_sheet(CSSStyleSheet const&);
+ struct AnimationKey {
+ CSS::CSSStyleDeclaration const* source_declaration;
+ DOM::Element const* element;
+ };
+
private:
enum class ComputeStyleMode {
Normal,
@@ -126,17 +133,29 @@ private:
JS::NonnullGCPtr m_document;
+ struct AnimationKeyFrameSet {
+ struct ResolvedKeyFrame {
+ struct UseInitial { };
+ Array>, to_underlying(last_property_id) + 1> resolved_properties {};
+ };
+ RedBlackTree keyframes_by_key;
+ };
+
struct RuleCache {
HashMap> rules_by_id;
HashMap> rules_by_class;
HashMap> rules_by_tag_name;
Vector other_rules;
+
+ HashMap> rules_by_animation_keyframes;
};
NonnullOwnPtr make_rule_cache_for_cascade_origin(CascadeOrigin);
RuleCache const& rule_cache_for_cascade_origin(CascadeOrigin) const;
+ void ensure_animation_timer() const;
+
OwnPtr m_author_rule_cache;
OwnPtr m_user_agent_rule_cache;
@@ -145,6 +164,57 @@ private:
Length::FontMetrics m_default_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 iteration_count; // Infinite if not set.
+ CSS::AnimationDirection direction;
+ CSS::AnimationFillMode fill_mode;
+ WeakPtr owning_element;
+
+ CSS::Percentage progress { 0 };
+ CSS::Time remaining_delay { 0, CSS::Time::Type::Ms };
+ AnimationState current_state { AnimationState::Before };
+ mutable Array, to_underlying(last_property_id) + 1> initial_state {};
+
+ AnimationStepTransition step(CSS::Time const& time_step);
+ ErrorOr collect_into(StyleProperties&, RuleCache const&) const;
+ bool is_done() const;
+ };
+
+ mutable HashMap> m_active_animations;
+ mutable RefPtr m_animation_driver_timer;
};
}
+
+template<>
+struct AK::Traits : public AK::GenericTraits {
+ 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;
+ }
+};
diff --git a/Userland/Libraries/LibWeb/Dump.cpp b/Userland/Libraries/LibWeb/Dump.cpp
index 98316e1817..332d0e11cf 100644
--- a/Userland/Libraries/LibWeb/Dump.cpp
+++ b/Userland/Libraries/LibWeb/Dump.cpp
@@ -652,6 +652,9 @@ ErrorOr dump_rule(StringBuilder& builder, CSS::CSSRule const& rule, int in
case CSS::CSSRule::Type::Supports:
TRY(dump_supports_rule(builder, verify_cast(rule), indent_levels));
break;
+ case CSS::CSSRule::Type::Keyframe:
+ case CSS::CSSRule::Type::Keyframes:
+ break;
}
return {};
}
diff --git a/Userland/Libraries/LibWeb/Forward.h b/Userland/Libraries/LibWeb/Forward.h
index d072d50821..d47b0ab8b7 100644
--- a/Userland/Libraries/LibWeb/Forward.h
+++ b/Userland/Libraries/LibWeb/Forward.h
@@ -68,16 +68,12 @@ class BackgroundStyleValue;
class BorderRadiusShorthandStyleValue;
class BorderRadiusStyleValue;
class BorderStyleValue;
-class CalculatedStyleValue;
-class Clip;
-class ColorStyleValue;
-class CompositeStyleValue;
-class ConicGradientStyleValue;
-class ContentStyleValue;
class CSSConditionRule;
class CSSFontFaceRule;
class CSSGroupingRule;
class CSSImportRule;
+class CSSKeyframeRule;
+class CSSKeyframesRule;
class CSSMediaRule;
class CSSRule;
class CSSRuleList;
@@ -85,6 +81,12 @@ class CSSStyleDeclaration;
class CSSStyleRule;
class CSSStyleSheet;
class CSSSupportsRule;
+class CalculatedStyleValue;
+class Clip;
+class ColorStyleValue;
+class CompositeStyleValue;
+class ConicGradientStyleValue;
+class ContentStyleValue;
class CustomIdentStyleValue;
class Display;
class DisplayStyleValue;
@@ -157,10 +159,10 @@ class TimeOrCalculated;
class TimePercentage;
class TimeStyleValue;
class TransformationStyleValue;
+class URLStyleValue;
class UnicodeRange;
class UnresolvedStyleValue;
class UnsetStyleValue;
-class URLStyleValue;
enum class MediaFeatureID;
enum class PropertyID;