Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions docs/validation/css-animation-pseudo-elements-676-242.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# CSS generated-pseudo animation source validation

Issue #676 extends WebScene's retained, eight-track CSS animation runtime under
#242 to generated `::before` and `::after` boxes. The originating element owns
each bounded pseudo timeline and receives its animation event with the standard
`pseudoElement` identity. No pseudo node or public ABI surface is introduced.

## Cascade and retained state

Animation shorthand and longhands cascade into the existing generated-pseudo
style record. Each eligible generated box compiles its maximum eight tracks
once after cascade, using the same bounded opacity, two-dimensional translate,
scale, rotation, and filter keyframes as an ordinary element. The retained
signature includes name, list-coordinated timing, delay, iteration, direction,
fill, play state, and compiled stops. A changed signature restarts only that
pseudo track; removing its rule, `content`, generated box, or originating
element clears its contribution and queued completion.

Sampling uses the host animation clock and the existing direction, fill,
pause, finite, fractional, infinite, and zero-time phase rules. Retained scene
commands bracket only that pseudo box for scale, rotation, and filters;
translation adjusts its retained box coordinates and animated opacity applies
to its generated paint. The principal element and the sibling pseudo box are
not included in those effect brackets.

## Ownership, lifecycle, and bounds

`animationend` dispatches exactly once on the originating element with
`pseudoElement` set to `::before` or `::after`. Identity mutation removes stale
queued pseudo events. Disconnected owners do not advance or request frames;
navigation and document teardown discard nodes, runtime tracks, and events.
Paused, completed, and fill-only pseudo tracks remain static without host-frame
demand.

Cascade and keyframe compilation occur only when style changes. A frame scans
at most eight retained tracks for each of the two generated boxes. Sampling
does no selector matching, cascade, keyframe parsing, DOM construction, or
visual-tree mutation. Allocation metrics include pseudo track and filter
storage.

## Evidence and deferred scope

`test_generated_pseudo_animations_share_owner_lifecycle` covers a running
`::before`, paused `::after`, retained midpoint transform paint, exact owner and
`pseudoElement` completion data, static fill, rule/content removal, detach, and
settled frame demand. The browser contract checks the same before/after
midpoint and owner-event shape against standard browser semantics.

Transitions on generated pseudo boxes, animationstart/animationiteration/
animationcancel expansion, CSS Animations on other pseudo types, Web Animations
API pseudo targets, additive composition, and properties outside the existing
bounded retained keyframe set remain deferred. Under the fast implementation
policy, only `git diff --check` is executed. Native, browser, package, and
unchanged Code OSS acceptance remain unexecuted.
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@ bool apply_native_cascade(native_document& document,dom_node& node,
if(node.style.after_pseudo().generated && previous.after_pseudo().generated)
node.style.mutable_after_pseudo().layout=previous.after_pseudo().layout;
configure_keyframes(node.style,sheets.state().opacity_keyframes);
if(node.style.has_pseudo_elements()) {
auto& before=node.style.mutable_before_pseudo();
auto& after=node.style.mutable_after_pseudo();
if(before.has_animation_data())
configure_keyframes(before.mutable_animations(),sheets.state().opacity_keyframes);
if(after.has_animation_data())
configure_keyframes(after.mutable_animations(),sheets.state().opacity_keyframes);
}
document.update_style_animations(node);
}
const bool layout_changed=!computed_layout_style_equal(previous,node.style);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#pragma once
#include "webscene_css_pseudo_values.h"
#include "webscene_css_text_values.h"
#include "webscene_css_transitions.h"
#include "webscene_css_variables.h"

namespace webscene_native::css {
Expand Down Expand Up @@ -124,6 +125,13 @@ void apply_pseudo_declaration(dom_node& node,node_style::pseudo_element& pseudo,
decision.classification = "supported";
return;
}
if (name.starts_with("animation")
&& apply_animation_property(pseudo.mutable_animations(), name, value)) {
decision.classification = "partially-supported";
decision.semantic_slice =
"bounded generated-pseudo keyframes on the originating element";
return;
}
const auto result = css::apply_pseudo_value(
pseudo, node.style.foreground_rgba, node.style.border_box, name, value);
if (name == "font-size" && result.classification == "supported") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,9 @@ inline bool apply_compiled_single_transition_shorthand(
return true;
}

inline void apply_animation_shorthand(node_style& style, const std::string& value)
inline void apply_animation_shorthand(
node_style::animation_data& animations,
const std::string& value)
{
std::vector<std::string> names, durations, delays, timings, iterations;
std::vector<std::string> directions, fill_modes, play_states;
Expand Down Expand Up @@ -477,7 +479,6 @@ inline void apply_animation_shorthand(node_style& style, const std::string& valu
}
return result;
};
auto& animations = style.mutable_animations();
animations.animation_name_value = names.empty() ? "none" : join(names);
animations.animation_duration_value = durations.empty() ? "0s" : join(durations);
animations.animation_delay_value = delays.empty() ? "0s" : join(delays);
Expand All @@ -488,6 +489,31 @@ inline void apply_animation_shorthand(node_style& style, const std::string& valu
animations.animation_play_state_value = play_states.empty() ? "running" : join(play_states);
}

inline void apply_animation_shorthand(node_style& style, const std::string& value)
{
apply_animation_shorthand(style.mutable_animations(), value);
}

inline bool apply_animation_property(
node_style::animation_data& animations,
std::string_view name,
const std::string& value)
{
if (name == "animation") apply_animation_shorthand(animations, value);
else if (name == "animation-name") animations.animation_name_value = value;
else if (name == "animation-duration") animations.animation_duration_value = value;
else if (name == "animation-delay") animations.animation_delay_value = value;
else if (name == "animation-timing-function") {
animations.animation_timing_function_value = value;
} else if (name == "animation-iteration-count") {
animations.animation_iteration_count_value = value;
} else if (name == "animation-direction") animations.animation_direction_value = value;
else if (name == "animation-fill-mode") animations.animation_fill_mode_value = value;
else if (name == "animation-play-state") animations.animation_play_state_value = value;
else return false;
return true;
}

inline std::string serialize_animation_shorthand(
const node_style::animation_data& animations)
{
Expand Down Expand Up @@ -523,11 +549,9 @@ inline std::string serialize_animation_shorthand(
return result;
}

inline void configure_keyframes(node_style& style,
inline void configure_keyframes(node_style::animation_data& animations,
const std::unordered_map<std::string,css_opacity_keyframes>& definitions)
{
if (!style.has_animation_data()) return;
auto& animations = style.mutable_animations();
animations.keyframe_animations.clear();
const auto names = split_css_component_list(animations.animation_name_value, ',');
if (names.empty()) return;
Expand Down Expand Up @@ -638,6 +662,13 @@ inline void configure_keyframes(node_style& style,
}
}

inline void configure_keyframes(node_style& style,
const std::unordered_map<std::string,css_opacity_keyframes>& definitions)
{
if (!style.has_animation_data()) return;
configure_keyframes(style.mutable_animations(), definitions);
}

inline void append_keyframe(
css_opacity_keyframes& definition,
std::string selector,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,35 @@ struct node_style final {
bool align_self_specified{false};
bool border_box{false};
bool elliptical_border_radius{false};

const animation_data& animations() const noexcept
{
static const animation_data defaults;
return animation_state == nullptr ? defaults : *animation_state;
}

animation_data& mutable_animations()
{
if (animation_state == nullptr) {
animation_state = std::make_shared<animation_data>();
} else if (animation_state.use_count() != 1) {
animation_state = std::make_shared<animation_data>(*animation_state);
}
return *animation_state;
}

bool has_animation_data() const noexcept
{
return animation_state != nullptr;
}

const animation_data* animation_data_identity() const noexcept
{
return animation_state.get();
}

private:
std::shared_ptr<animation_data> animation_state;
};

struct pseudo_element_pair final {
Expand Down Expand Up @@ -1493,6 +1522,19 @@ struct dom_node final {
bool filter_valid{false};
bool end_event_sent{false};
};
struct pseudo_animation_runtime final {
std::vector<keyframe_animation_runtime> keyframe_animations;
std::vector<retained_filter_function> painted_filter_functions;
css_length painted_translation_x{0, length_unit::pixels};
css_length painted_translation_y{0, length_unit::pixels};
float painted_scale_x{1};
float painted_scale_y{1};
float painted_rotation_degrees{0};
float painted_opacity{1};
bool opacity_override{false};
bool transform_override{false};
bool filter_override{false};
};
css_length painted_transform_translate_x{};
css_length painted_transform_translate_y{};
css_length transform_animation_from_translate_x{};
Expand Down Expand Up @@ -1558,6 +1600,8 @@ struct dom_node final {
bool opacity_animation_active{false};
bool opacity_animation_start_event_sent{false};
std::vector<keyframe_animation_runtime> keyframe_animations;
pseudo_animation_runtime before_pseudo;
pseudo_animation_runtime after_pseudo;
bool keyframe_opacity_override{false};
bool keyframe_translation_override{false};
bool keyframe_scale_override{false};
Expand Down Expand Up @@ -2024,6 +2068,7 @@ class native_document final {
std::string type;
std::string animation_name;
float elapsed_time_seconds{0};
std::string pseudo_element;
};

explicit native_document(
Expand Down
Loading
Loading