1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 07:28:11 +00:00

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>
This commit is contained in:
Ali Mohammad Pur 2023-05-26 23:30:54 +03:30 committed by Andreas Kling
parent f07c4ffbc8
commit e90752cc21
31 changed files with 1062 additions and 12 deletions

View file

@ -15,6 +15,8 @@
#include <LibWeb/Bindings/MainThreadVM.h>
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSKeyframeRule.h>
#include <LibWeb/CSS/CSSKeyframesRule.h>
#include <LibWeb/CSS/CSSMediaRule.h>
#include <LibWeb/CSS/CSSStyleDeclaration.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();
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!
dbgln_if(CSS_PARSER_DEBUG, "Unrecognized CSS at-rule: @{}", rule->at_rule_name());