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

@ -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

View file

@ -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>

View file

@ -163,6 +163,7 @@
<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="effects_with_opacity_and_transforms.html">Effects with opacity and transforms</a></li>
<li><a href="css-animations.html">CSS Animations</a></li>
</ul>
<h2>JavaScript/Wasm</h2>

View file

@ -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)

View file

@ -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

View file

@ -22,6 +22,12 @@ void CSSConditionRule::for_each_effective_style_rule(Function<void(CSSStyleRule
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)
{
MUST_OR_THROW_OOM(Base::initialize(realm));

View file

@ -23,6 +23,7 @@ public:
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_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const override;
protected:
CSSConditionRule(JS::Realm&, CSSRuleList&);

View file

@ -54,6 +54,11 @@ void CSSGroupingRule::for_each_effective_style_rule(Function<void(CSSStyleRule c
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)
{
CSSRule::set_parent_style_sheet(parent_style_sheet);

View file

@ -27,6 +27,7 @@ public:
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_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const;
virtual void set_parent_style_sheet(CSSStyleSheet*) override;

View file

@ -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();
}
}

View file

@ -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; }
}

View file

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

View file

@ -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();
}
}

View file

@ -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; }
}

View file

@ -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);
};

View file

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

View file

@ -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;

View file

@ -8,6 +8,7 @@
#include <LibWeb/Bindings/CSSRuleListPrototype.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CSS/CSSImportRule.h>
#include <LibWeb/CSS/CSSKeyframesRule.h>
#include <LibWeb/CSS/CSSMediaRule.h>
#include <LibWeb/CSS/CSSRule.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:
static_cast<CSSSupportsRule const&>(*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<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;
break;
}
case CSSRule::Type::Keyframe:
case CSSRule::Type::Keyframes:
break;
}
}

View file

@ -59,6 +59,7 @@ public:
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.
bool evaluate_media_queries(HTML::Window const&);
void for_each_effective_keyframes_at_rule(Function<void(CSSKeyframesRule const&)> const& callback) const;
private:
explicit CSSRuleList(JS::Realm&);

View file

@ -53,7 +53,8 @@ class PropertyOwningCSSStyleDeclaration : public CSSStyleDeclaration {
friend class ElementInlineCSSStyleDeclaration;
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;

View file

@ -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 any_media_queries_changed_match_state = false;

View file

@ -45,6 +45,7 @@ public:
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.
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*);

View file

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

View file

@ -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",

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());

View file

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

View file

@ -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": [

View file

@ -986,6 +986,297 @@ static ErrorOr<void> cascade_custom_properties(DOM::Element& element, Optional<C
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
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
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
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;
});
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;
});

View file

@ -10,7 +10,9 @@
#include <AK/HashMap.h>
#include <AK/Optional.h>
#include <AK/OwnPtr.h>
#include <AK/RedBlackTree.h>
#include <LibWeb/CSS/CSSFontFaceRule.h>
#include <LibWeb/CSS/CSSKeyframesRule.h>
#include <LibWeb/CSS/CSSStyleDeclaration.h>
#include <LibWeb/CSS/Parser/ComponentValue.h>
#include <LibWeb/CSS/Parser/TokenStream.h>
@ -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<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 {
HashMap<FlyString, Vector<MatchingRule>> rules_by_id;
HashMap<FlyString, Vector<MatchingRule>> rules_by_class;
HashMap<FlyString, Vector<MatchingRule>> rules_by_tag_name;
Vector<MatchingRule> other_rules;
HashMap<FlyString, NonnullOwnPtr<AnimationKeyFrameSet>> rules_by_animation_keyframes;
};
NonnullOwnPtr<RuleCache> make_rule_cache_for_cascade_origin(CascadeOrigin);
RuleCache const& rule_cache_for_cascade_origin(CascadeOrigin) const;
void ensure_animation_timer() const;
OwnPtr<RuleCache> m_author_rule_cache;
OwnPtr<RuleCache> 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<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;
}
};

View file

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

View file

@ -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;