Skip to content

Commit 789f4d3

Browse files
Animate generated pseudo elements (#676) (#686)
1 parent 771787b commit 789f4d3

16 files changed

Lines changed: 845 additions & 15 deletions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# CSS generated-pseudo animation source validation
2+
3+
Issue #676 extends WebScene's retained, eight-track CSS animation runtime under
4+
#242 to generated `::before` and `::after` boxes. The originating element owns
5+
each bounded pseudo timeline and receives its animation event with the standard
6+
`pseudoElement` identity. No pseudo node or public ABI surface is introduced.
7+
8+
## Cascade and retained state
9+
10+
Animation shorthand and longhands cascade into the existing generated-pseudo
11+
style record. Each eligible generated box compiles its maximum eight tracks
12+
once after cascade, using the same bounded opacity, two-dimensional translate,
13+
scale, rotation, and filter keyframes as an ordinary element. The retained
14+
signature includes name, list-coordinated timing, delay, iteration, direction,
15+
fill, play state, and compiled stops. A changed signature restarts only that
16+
pseudo track; removing its rule, `content`, generated box, or originating
17+
element clears its contribution and queued completion.
18+
19+
Sampling uses the host animation clock and the existing direction, fill,
20+
pause, finite, fractional, infinite, and zero-time phase rules. Retained scene
21+
commands bracket only that pseudo box for scale, rotation, and filters;
22+
translation adjusts its retained box coordinates and animated opacity applies
23+
to its generated paint. The principal element and the sibling pseudo box are
24+
not included in those effect brackets.
25+
26+
## Ownership, lifecycle, and bounds
27+
28+
`animationend` dispatches exactly once on the originating element with
29+
`pseudoElement` set to `::before` or `::after`. Identity mutation removes stale
30+
queued pseudo events. Disconnected owners do not advance or request frames;
31+
navigation and document teardown discard nodes, runtime tracks, and events.
32+
Paused, completed, and fill-only pseudo tracks remain static without host-frame
33+
demand.
34+
35+
Cascade and keyframe compilation occur only when style changes. A frame scans
36+
at most eight retained tracks for each of the two generated boxes. Sampling
37+
does no selector matching, cascade, keyframe parsing, DOM construction, or
38+
visual-tree mutation. Allocation metrics include pseudo track and filter
39+
storage.
40+
41+
## Evidence and deferred scope
42+
43+
`test_generated_pseudo_animations_share_owner_lifecycle` covers a running
44+
`::before`, paused `::after`, retained midpoint transform paint, exact owner and
45+
`pseudoElement` completion data, static fill, rule/content removal, detach, and
46+
settled frame demand. The browser contract checks the same before/after
47+
midpoint and owner-event shape against standard browser semantics.
48+
49+
Transitions on generated pseudo boxes, animationstart/animationiteration/
50+
animationcancel expansion, CSS Animations on other pseudo types, Web Animations
51+
API pseudo targets, additive composition, and properties outside the existing
52+
bounded retained keyframe set remain deferred. Under the fast implementation
53+
policy, only `git diff --check` is executed. Native, browser, package, and
54+
unchanged Code OSS acceptance remain unexecuted.

‎experiments/WebScene.NativeEngine.Probe/native/webscene_css_native_cascade.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,14 @@ bool apply_native_cascade(native_document& document,dom_node& node,
8181
if(node.style.after_pseudo().generated && previous.after_pseudo().generated)
8282
node.style.mutable_after_pseudo().layout=previous.after_pseudo().layout;
8383
configure_keyframes(node.style,sheets.state().opacity_keyframes);
84+
if(node.style.has_pseudo_elements()) {
85+
auto& before=node.style.mutable_before_pseudo();
86+
auto& after=node.style.mutable_after_pseudo();
87+
if(before.has_animation_data())
88+
configure_keyframes(before.mutable_animations(),sheets.state().opacity_keyframes);
89+
if(after.has_animation_data())
90+
configure_keyframes(after.mutable_animations(),sheets.state().opacity_keyframes);
91+
}
8492
document.update_style_animations(node);
8593
}
8694
const bool layout_changed=!computed_layout_style_equal(previous,node.style);

‎experiments/WebScene.NativeEngine.Probe/native/webscene_css_pseudo_application.h‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#pragma once
22
#include "webscene_css_pseudo_values.h"
33
#include "webscene_css_text_values.h"
4+
#include "webscene_css_transitions.h"
45
#include "webscene_css_variables.h"
56

67
namespace webscene_native::css {
@@ -124,6 +125,13 @@ void apply_pseudo_declaration(dom_node& node,node_style::pseudo_element& pseudo,
124125
decision.classification = "supported";
125126
return;
126127
}
128+
if (name.starts_with("animation")
129+
&& apply_animation_property(pseudo.mutable_animations(), name, value)) {
130+
decision.classification = "partially-supported";
131+
decision.semantic_slice =
132+
"bounded generated-pseudo keyframes on the originating element";
133+
return;
134+
}
127135
const auto result = css::apply_pseudo_value(
128136
pseudo, node.style.foreground_rgba, node.style.border_box, name, value);
129137
if (name == "font-size" && result.classification == "supported") {

‎experiments/WebScene.NativeEngine.Probe/native/webscene_css_transitions.h‎

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,9 @@ inline bool apply_compiled_single_transition_shorthand(
420420
return true;
421421
}
422422

423-
inline void apply_animation_shorthand(node_style& style, const std::string& value)
423+
inline void apply_animation_shorthand(
424+
node_style::animation_data& animations,
425+
const std::string& value)
424426
{
425427
std::vector<std::string> names, durations, delays, timings, iterations;
426428
std::vector<std::string> directions, fill_modes, play_states;
@@ -477,7 +479,6 @@ inline void apply_animation_shorthand(node_style& style, const std::string& valu
477479
}
478480
return result;
479481
};
480-
auto& animations = style.mutable_animations();
481482
animations.animation_name_value = names.empty() ? "none" : join(names);
482483
animations.animation_duration_value = durations.empty() ? "0s" : join(durations);
483484
animations.animation_delay_value = delays.empty() ? "0s" : join(delays);
@@ -488,6 +489,31 @@ inline void apply_animation_shorthand(node_style& style, const std::string& valu
488489
animations.animation_play_state_value = play_states.empty() ? "running" : join(play_states);
489490
}
490491

492+
inline void apply_animation_shorthand(node_style& style, const std::string& value)
493+
{
494+
apply_animation_shorthand(style.mutable_animations(), value);
495+
}
496+
497+
inline bool apply_animation_property(
498+
node_style::animation_data& animations,
499+
std::string_view name,
500+
const std::string& value)
501+
{
502+
if (name == "animation") apply_animation_shorthand(animations, value);
503+
else if (name == "animation-name") animations.animation_name_value = value;
504+
else if (name == "animation-duration") animations.animation_duration_value = value;
505+
else if (name == "animation-delay") animations.animation_delay_value = value;
506+
else if (name == "animation-timing-function") {
507+
animations.animation_timing_function_value = value;
508+
} else if (name == "animation-iteration-count") {
509+
animations.animation_iteration_count_value = value;
510+
} else if (name == "animation-direction") animations.animation_direction_value = value;
511+
else if (name == "animation-fill-mode") animations.animation_fill_mode_value = value;
512+
else if (name == "animation-play-state") animations.animation_play_state_value = value;
513+
else return false;
514+
return true;
515+
}
516+
491517
inline std::string serialize_animation_shorthand(
492518
const node_style::animation_data& animations)
493519
{
@@ -523,11 +549,9 @@ inline std::string serialize_animation_shorthand(
523549
return result;
524550
}
525551

526-
inline void configure_keyframes(node_style& style,
552+
inline void configure_keyframes(node_style::animation_data& animations,
527553
const std::unordered_map<std::string,css_opacity_keyframes>& definitions)
528554
{
529-
if (!style.has_animation_data()) return;
530-
auto& animations = style.mutable_animations();
531555
animations.keyframe_animations.clear();
532556
const auto names = split_css_component_list(animations.animation_name_value, ',');
533557
if (names.empty()) return;
@@ -638,6 +662,13 @@ inline void configure_keyframes(node_style& style,
638662
}
639663
}
640664

665+
inline void configure_keyframes(node_style& style,
666+
const std::unordered_map<std::string,css_opacity_keyframes>& definitions)
667+
{
668+
if (!style.has_animation_data()) return;
669+
configure_keyframes(style.mutable_animations(), definitions);
670+
}
671+
641672
inline void append_keyframe(
642673
css_opacity_keyframes& definition,
643674
std::string selector,

‎experiments/WebScene.NativeEngine.Probe/native/webscene_native_dom.h‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,35 @@ struct node_style final {
539539
bool align_self_specified{false};
540540
bool border_box{false};
541541
bool elliptical_border_radius{false};
542+
543+
const animation_data& animations() const noexcept
544+
{
545+
static const animation_data defaults;
546+
return animation_state == nullptr ? defaults : *animation_state;
547+
}
548+
549+
animation_data& mutable_animations()
550+
{
551+
if (animation_state == nullptr) {
552+
animation_state = std::make_shared<animation_data>();
553+
} else if (animation_state.use_count() != 1) {
554+
animation_state = std::make_shared<animation_data>(*animation_state);
555+
}
556+
return *animation_state;
557+
}
558+
559+
bool has_animation_data() const noexcept
560+
{
561+
return animation_state != nullptr;
562+
}
563+
564+
const animation_data* animation_data_identity() const noexcept
565+
{
566+
return animation_state.get();
567+
}
568+
569+
private:
570+
std::shared_ptr<animation_data> animation_state;
542571
};
543572

544573
struct pseudo_element_pair final {
@@ -1493,6 +1522,19 @@ struct dom_node final {
14931522
bool filter_valid{false};
14941523
bool end_event_sent{false};
14951524
};
1525+
struct pseudo_animation_runtime final {
1526+
std::vector<keyframe_animation_runtime> keyframe_animations;
1527+
std::vector<retained_filter_function> painted_filter_functions;
1528+
css_length painted_translation_x{0, length_unit::pixels};
1529+
css_length painted_translation_y{0, length_unit::pixels};
1530+
float painted_scale_x{1};
1531+
float painted_scale_y{1};
1532+
float painted_rotation_degrees{0};
1533+
float painted_opacity{1};
1534+
bool opacity_override{false};
1535+
bool transform_override{false};
1536+
bool filter_override{false};
1537+
};
14961538
css_length painted_transform_translate_x{};
14971539
css_length painted_transform_translate_y{};
14981540
css_length transform_animation_from_translate_x{};
@@ -1558,6 +1600,8 @@ struct dom_node final {
15581600
bool opacity_animation_active{false};
15591601
bool opacity_animation_start_event_sent{false};
15601602
std::vector<keyframe_animation_runtime> keyframe_animations;
1603+
pseudo_animation_runtime before_pseudo;
1604+
pseudo_animation_runtime after_pseudo;
15611605
bool keyframe_opacity_override{false};
15621606
bool keyframe_translation_override{false};
15631607
bool keyframe_scale_override{false};
@@ -2024,6 +2068,7 @@ class native_document final {
20242068
std::string type;
20252069
std::string animation_name;
20262070
float elapsed_time_seconds{0};
2071+
std::string pseudo_element;
20272072
};
20282073

20292074
explicit native_document(

0 commit comments

Comments
 (0)