mirror of
https://github.com/SerenityOS/serenity
synced 2024-09-06 08:56:40 +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:
parent
f07c4ffbc8
commit
e90752cc21
|
@ -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
|
||||
|
|
50
Base/res/html/misc/css-animations.html
Normal file
50
Base/res/html/misc/css-animations.html
Normal 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>
|
|
@ -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>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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&);
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
30
Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.cpp
Normal file
30
Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
54
Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.h
Normal file
54
Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.h
Normal 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; }
|
||||
|
||||
}
|
7
Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.idl
Normal file
7
Userland/Libraries/LibWeb/CSS/CSSKeyframeRule.idl
Normal file
|
@ -0,0 +1,7 @@
|
|||
#import <CSS/CSSRule.idl>
|
||||
|
||||
[Exposed = Window]
|
||||
interface CSSKeyframeRule : CSSRule {
|
||||
attribute CSSOMString keyText;
|
||||
[SameObject, PutForwards=cssText] readonly attribute CSSStyleDeclaration style;
|
||||
};
|
36
Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.cpp
Normal file
36
Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.cpp
Normal 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();
|
||||
}
|
||||
|
||||
}
|
56
Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.h
Normal file
56
Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.h
Normal 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; }
|
||||
|
||||
}
|
13
Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.idl
Normal file
13
Userland/Libraries/LibWeb/CSS/CSSKeyframesRule.idl
Normal 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);
|
||||
};
|
|
@ -27,6 +27,8 @@ public:
|
|||
Import = 3,
|
||||
Media = 4,
|
||||
FontFace = 5,
|
||||
Keyframes = 7,
|
||||
Keyframe = 8,
|
||||
Supports = 12,
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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&);
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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*);
|
||||
|
||||
|
|
|
@ -32,6 +32,18 @@
|
|||
"stretch",
|
||||
"unsafe"
|
||||
],
|
||||
"animation-fill-mode": [
|
||||
"backwards",
|
||||
"both",
|
||||
"forwards",
|
||||
"none"
|
||||
],
|
||||
"animation-direction": [
|
||||
"alternate",
|
||||
"alternate-reverse",
|
||||
"normal",
|
||||
"reverse"
|
||||
],
|
||||
"appearance": [
|
||||
"auto",
|
||||
"button",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -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 {};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue