Retain clean styles across animation frames
This commit is contained in:
1 parent
fd87ddd0b9
commit
47e9aa096b
5 files changed
+341
-10
No files matched your search
@@ -3427,18 +3427,22 @@ pub(crate) fn ensure_prepared_render(
|
|||||||
let base_url = document_base_url(state);
|
let base_url = document_base_url(state);
|
||||||
let viewport = state.viewport;
|
let viewport = state.viewport;
|
||||||
let animation_sample = state.animation_sample;
|
let animation_sample = state.animation_sample;
|
||||||
let stale = state.prepared_render.as_ref().map_or(true, |prepared| {
|
let incompatible = state.prepared_render.as_ref().is_some_and(|prepared| {
|
||||||
prepared.viewport() != viewport
|
prepared.viewport() != viewport
|
||||||
|| prepared.base_url() != base_url.as_deref()
|
|| prepared.base_url() != base_url.as_deref()
|
||||||
|| prepared.animation_sample() != animation_sample
|
|
||||||
});
|
});
|
||||||
if stale || !state.pending_style_mutations.is_empty() {
|
let needs_rebuild = state.prepared_render.as_ref().map_or(true, |prepared| {
|
||||||
|
incompatible || prepared.animation_sample() != animation_sample
|
||||||
|
}) || !state.pending_style_mutations.is_empty();
|
||||||
|
if needs_rebuild {
|
||||||
if let Some(dom) = state.dom.as_ref() {
|
if let Some(dom) = state.dom.as_ref() {
|
||||||
state
|
state
|
||||||
.animation_timeline
|
.animation_timeline
|
||||||
.materialize_start_candidates(dom);
|
.materialize_start_candidates(dom);
|
||||||
}
|
}
|
||||||
let previous = (!stale).then(|| state.prepared_render.take()).flatten();
|
let previous = (!incompatible)
|
||||||
|
.then(|| state.prepared_render.take())
|
||||||
|
.flatten();
|
||||||
let mutations = std::mem::take(&mut state.pending_style_mutations);
|
let mutations = std::mem::take(&mut state.pending_style_mutations);
|
||||||
let prepared = {
|
let prepared = {
|
||||||
let dom = state.dom.as_ref()?;
|
let dom = state.dom.as_ref()?;
|
||||||
@@ -3520,9 +3524,15 @@ pub(crate) fn sample_live_document_animations(state: &mut ObscuraState) {
|
|||||||
state.animation_sample = sample;
|
state.animation_sample = sample;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let forward_document_sample =
|
||||||
|
sample.mode == obscura_render::AnimationSampleMode::DocumentTime
|
||||||
|
&& state.animation_sample.mode == obscura_render::AnimationSampleMode::DocumentTime
|
||||||
|
&& sample.time.milliseconds > state.animation_sample.time.milliseconds;
|
||||||
state.animation_sample = sample;
|
state.animation_sample = sample;
|
||||||
|
if !forward_document_sample {
|
||||||
state.prepared_render = None;
|
state.prepared_render = None;
|
||||||
state.pending_style_mutations.clear();
|
state.pending_style_mutations.clear();
|
||||||
|
}
|
||||||
state.resolved_scroll = None;
|
state.resolved_scroll = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -471,9 +471,11 @@ impl ObscuraJsRuntime {
|
|||||||
}
|
}
|
||||||
let mut state = self.state.borrow_mut();
|
let mut state = self.state.borrow_mut();
|
||||||
if state.animation_sample != sample {
|
if state.animation_sample != sample {
|
||||||
if sample.mode == obscura_render::AnimationSampleMode::DocumentTime
|
let forward_document_sample =
|
||||||
|
sample.mode == obscura_render::AnimationSampleMode::DocumentTime
|
||||||
&& state.animation_sample.mode == obscura_render::AnimationSampleMode::DocumentTime
|
&& state.animation_sample.mode == obscura_render::AnimationSampleMode::DocumentTime
|
||||||
&& sample.time.milliseconds > state.animation_sample.time.milliseconds
|
&& sample.time.milliseconds > state.animation_sample.time.milliseconds;
|
||||||
|
if forward_document_sample
|
||||||
&& state.prepared_render.as_mut().is_some_and(|prepared| {
|
&& state.prepared_render.as_mut().is_some_and(|prepared| {
|
||||||
prepared.advance_inactive_animation_sample_time(sample.time)
|
prepared.advance_inactive_animation_sample_time(sample.time)
|
||||||
})
|
})
|
||||||
@@ -482,8 +484,10 @@ impl ObscuraJsRuntime {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
state.animation_sample = sample;
|
state.animation_sample = sample;
|
||||||
|
if !forward_document_sample {
|
||||||
state.prepared_render = None;
|
state.prepared_render = None;
|
||||||
state.pending_style_mutations.clear();
|
state.pending_style_mutations.clear();
|
||||||
|
}
|
||||||
state.resolved_scroll = None;
|
state.resolved_scroll = None;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -6879,6 +6883,100 @@ mod tests {
|
|||||||
assert_eq!(first, second);
|
assert_eq!(first, second);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[test]
|
||||||
|
fn forward_active_animation_sample_updates_geometry_and_paint_from_retained_frame() {
|
||||||
|
let mut rt = ObscuraJsRuntime::new();
|
||||||
|
rt.set_dom(parse_html(
|
||||||
|
r#"<html style="margin:0"><head><style>
|
||||||
|
@keyframes grow {
|
||||||
|
from { width:20px; background-color:#ff0000 }
|
||||||
|
to { width:100px; background-color:#0000ff }
|
||||||
|
}
|
||||||
|
#box { height:40px; animation:grow 1000ms linear both }
|
||||||
|
</style></head><body style="margin:0"><div id="box"></div></body></html>"#,
|
||||||
|
));
|
||||||
|
rt.set_url("http://example.test/page");
|
||||||
|
rt.set_viewport(120.0, 40.0);
|
||||||
|
rt.run_page_init();
|
||||||
|
|
||||||
|
assert!(rt.set_animation_sample(obscura_render::AnimationSample::document(0.0)));
|
||||||
|
let initial = rt
|
||||||
|
.screenshot_prepared((120.0, 40.0), Some("http://example.test/page"))
|
||||||
|
.expect("initial animation frame");
|
||||||
|
assert!((animation_test_width(&rt, "box") - 20.0).abs() < 0.1);
|
||||||
|
|
||||||
|
assert!(rt.set_animation_sample(obscura_render::AnimationSample::document(500.0)));
|
||||||
|
assert!(
|
||||||
|
rt.state.borrow().prepared_render.is_some(),
|
||||||
|
"a forward active sample should retain the previous style graph until flush"
|
||||||
|
);
|
||||||
|
let midpoint = rt
|
||||||
|
.screenshot_prepared((120.0, 40.0), Some("http://example.test/page"))
|
||||||
|
.expect("retained midpoint animation frame");
|
||||||
|
|
||||||
|
assert!((animation_test_width(&rt, "box") - 60.0).abs() < 0.1);
|
||||||
|
assert_ne!(initial, midpoint, "animated paint output must advance");
|
||||||
|
assert_eq!(
|
||||||
|
rt.state
|
||||||
|
.borrow()
|
||||||
|
.prepared_render
|
||||||
|
.as_ref()
|
||||||
|
.unwrap()
|
||||||
|
.animation_sample_time()
|
||||||
|
.milliseconds,
|
||||||
|
500.0
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "render")]
|
||||||
|
#[test]
|
||||||
|
fn forward_waapi_sample_updates_retained_style_and_paint() {
|
||||||
|
let mut rt = setup_runtime(
|
||||||
|
r#"<html style="margin:0"><body style="margin:0">
|
||||||
|
<div id="box" style="width:40px;height:40px;background:#1769aa"></div>
|
||||||
|
</body></html>"#,
|
||||||
|
);
|
||||||
|
rt.set_viewport(120.0, 40.0);
|
||||||
|
rt.state.borrow_mut().animation_timeline_origin = std::time::Instant::now();
|
||||||
|
assert!(rt.set_animation_sample(obscura_render::AnimationSample::document(0.0)));
|
||||||
|
rt.execute_script(
|
||||||
|
"waapi-retained-frame",
|
||||||
|
r#"document.getElementById('box').animate(
|
||||||
|
[{opacity:0,transform:'translateX(0px)'},
|
||||||
|
{opacity:1,transform:'translateX(80px)'}],
|
||||||
|
{duration:1000,fill:'both',easing:'linear'}
|
||||||
|
)"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let initial = rt
|
||||||
|
.screenshot_prepared((120.0, 40.0), Some("http://example.com/test"))
|
||||||
|
.expect("initial WAAPI frame");
|
||||||
|
|
||||||
|
assert!(rt.set_animation_sample(obscura_render::AnimationSample::document(500.0)));
|
||||||
|
assert!(
|
||||||
|
rt.state.borrow().prepared_render.is_some(),
|
||||||
|
"a forward WAAPI sample should preserve the prepared style graph until flush"
|
||||||
|
);
|
||||||
|
let midpoint = rt
|
||||||
|
.screenshot_prepared((120.0, 40.0), Some("http://example.com/test"))
|
||||||
|
.expect("retained WAAPI midpoint");
|
||||||
|
let midpoint_opacity = {
|
||||||
|
let state = rt.state.borrow();
|
||||||
|
let dom = state.dom.as_ref().unwrap();
|
||||||
|
let box_node = dom.get_element_by_id("box").unwrap();
|
||||||
|
state.prepared_render.as_ref().unwrap().layout().styles[&box_node]
|
||||||
|
.opacity
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
(0.45..0.55).contains(&midpoint_opacity),
|
||||||
|
"WAAPI midpoint opacity={midpoint_opacity}"
|
||||||
|
);
|
||||||
|
assert_ne!(initial, midpoint, "WAAPI paint output must advance");
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "render")]
|
#[cfg(feature = "render")]
|
||||||
#[test]
|
#[test]
|
||||||
fn completed_animation_retains_forward_frame_but_backward_seek_rebuilds() {
|
fn completed_animation_retains_forward_frame_but_backward_seek_rebuilds() {
|
||||||
|
|||||||
@@ -540,6 +540,10 @@ pub enum TreeStyleMutation {
|
|||||||
pub enum RetainedStyleMutation {
|
pub enum RetainedStyleMutation {
|
||||||
Attribute(AttributeStyleMutation),
|
Attribute(AttributeStyleMutation),
|
||||||
Tree(TreeStyleMutation),
|
Tree(TreeStyleMutation),
|
||||||
|
/// A document-timeline sample changed while the DOM and stylesheet stayed
|
||||||
|
/// stable. Re-cascade this animation owner and its inheritance-dependent
|
||||||
|
/// subtree while retaining unrelated branches.
|
||||||
|
Animation { node: NodeId },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<AttributeStyleMutation> for RetainedStyleMutation {
|
impl From<AttributeStyleMutation> for RetainedStyleMutation {
|
||||||
@@ -2594,7 +2598,10 @@ fn container_iteration_termination<T: PartialEq>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
enum RetainedStylePlan {
|
enum RetainedStylePlan {
|
||||||
Reuse(HashSet<NodeId>),
|
Reuse {
|
||||||
|
dirty: HashSet<NodeId>,
|
||||||
|
has_animation_damage: bool,
|
||||||
|
},
|
||||||
Full,
|
Full,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3126,6 +3133,7 @@ fn empty_state_may_have_changed(
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|mutation| match mutation {
|
.filter(|mutation| match mutation {
|
||||||
RetainedStyleMutation::Attribute(_) => false,
|
RetainedStyleMutation::Attribute(_) => false,
|
||||||
|
RetainedStyleMutation::Animation { .. } => false,
|
||||||
RetainedStyleMutation::Tree(TreeStyleMutation::Insert {
|
RetainedStyleMutation::Tree(TreeStyleMutation::Insert {
|
||||||
old_parent,
|
old_parent,
|
||||||
new_parent,
|
new_parent,
|
||||||
@@ -3189,7 +3197,17 @@ fn retained_style_plan(
|
|||||||
mutations: &[RetainedStyleMutation],
|
mutations: &[RetainedStyleMutation],
|
||||||
) -> RetainedStylePlan {
|
) -> RetainedStylePlan {
|
||||||
let mut dirty = HashSet::new();
|
let mut dirty = HashSet::new();
|
||||||
|
let mut has_animation_damage = false;
|
||||||
for mutation in mutations {
|
for mutation in mutations {
|
||||||
|
if let RetainedStyleMutation::Animation { node } = mutation {
|
||||||
|
// Animated color and visibility inherit, keyframe endpoints can
|
||||||
|
// consume inherited custom properties, and generated pseudos are
|
||||||
|
// rebuilt with their originating element. The existing subtree
|
||||||
|
// expansion covers all three while retaining sibling branches.
|
||||||
|
add_style_subtree(tree, *node, &mut dirty);
|
||||||
|
has_animation_damage = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let RetainedStyleMutation::Attribute(mutation) = mutation else {
|
let RetainedStyleMutation::Attribute(mutation) = mutation else {
|
||||||
let RetainedStyleMutation::Tree(mutation) = mutation else {
|
let RetainedStyleMutation::Tree(mutation) = mutation else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
@@ -3353,7 +3371,10 @@ fn retained_style_plan(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RetainedStylePlan::Reuse(dirty)
|
RetainedStylePlan::Reuse {
|
||||||
|
dirty,
|
||||||
|
has_animation_damage,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn layout_dom_with_web_fonts(
|
pub(crate) fn layout_dom_with_web_fonts(
|
||||||
@@ -3599,7 +3620,10 @@ fn layout_dom_with_web_fonts_pass_limit_at_animation_time(
|
|||||||
.custom_properties
|
.custom_properties
|
||||||
.retain(|node, _| connected.contains(node));
|
.retain(|node, _| connected.contains(node));
|
||||||
match retained_style_plan(tree, &sheet, mutations) {
|
match retained_style_plan(tree, &sheet, mutations) {
|
||||||
RetainedStylePlan::Reuse(mut dirty) => {
|
RetainedStylePlan::Reuse {
|
||||||
|
mut dirty,
|
||||||
|
has_animation_damage,
|
||||||
|
} => {
|
||||||
if !active_containers.is_empty() && sheet.has_container_queries() {
|
if !active_containers.is_empty() && sheet.has_container_queries() {
|
||||||
let mut matcher = tree.matcher();
|
let mut matcher = tree.matcher();
|
||||||
add_container_query_reset_scopes(
|
add_container_query_reset_scopes(
|
||||||
@@ -3613,6 +3637,22 @@ fn layout_dom_with_web_fonts_pass_limit_at_animation_time(
|
|||||||
&mut dirty,
|
&mut dirty,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// Once animation damage reaches at least half of the retained
|
||||||
|
// style graph, sparse HashMap reuse no longer offsets dirty-set
|
||||||
|
// bookkeeping and branch checks. This is a document-relative
|
||||||
|
// coverage threshold, not a site- or node-count heuristic.
|
||||||
|
if has_animation_damage
|
||||||
|
&& dirty.len().saturating_mul(2) >= retained.styles.len()
|
||||||
|
{
|
||||||
|
if std::env::var_os("OBSCURA_RENDER_TIMING").is_some() {
|
||||||
|
eprintln!(
|
||||||
|
"[timing] retained-style fallback reason=animation-dirty-coverage dirty={} retained={}",
|
||||||
|
dirty.len(),
|
||||||
|
retained.styles.len(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
Some((retained, dirty))
|
Some((retained, dirty))
|
||||||
}
|
}
|
||||||
RetainedStylePlan::Full => None,
|
RetainedStylePlan::Full => None,
|
||||||
@@ -14581,6 +14621,87 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn retained_animation_restyle_reuses_clean_branches_and_matches_full() {
|
||||||
|
let mut clean = String::new();
|
||||||
|
for index in 0..2_000 {
|
||||||
|
clean.push_str(&format!(
|
||||||
|
"<span class=clean data-index={index}><i></i></span>"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let tree = parse_html(&format!(
|
||||||
|
r#"<style>
|
||||||
|
html,body{{margin:0}}
|
||||||
|
@keyframes grow {{
|
||||||
|
from {{ width:20px; color:#ff0000 }}
|
||||||
|
to {{ width:120px; color:#0000ff }}
|
||||||
|
}}
|
||||||
|
#animated{{height:20px;animation:grow 1000ms linear both}}
|
||||||
|
#animated > b{{color:inherit}}
|
||||||
|
.clean{{display:block;width:3px;height:1px}}
|
||||||
|
</style><main><section id=animated><b id=child>x</b></section>
|
||||||
|
<aside>{clean}</aside></main>"#,
|
||||||
|
));
|
||||||
|
let animated = tree.get_element_by_id("animated").unwrap();
|
||||||
|
let mut cache = crate::css::StylesheetCache::default();
|
||||||
|
let mut timeline = crate::AnimationTimelineState::default();
|
||||||
|
let (mut initial, _) = layout_dom_with_web_fonts_pass_limit_at_animation_time(
|
||||||
|
&tree,
|
||||||
|
(800.0, 600.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
Some(&mut cache),
|
||||||
|
None,
|
||||||
|
&[],
|
||||||
|
crate::AnimationSample::document(0.0),
|
||||||
|
&mut timeline,
|
||||||
|
);
|
||||||
|
let retained = RetainedStyleMaps {
|
||||||
|
styles: std::mem::take(&mut initial.styles),
|
||||||
|
custom_properties: std::mem::take(&mut initial.custom_properties),
|
||||||
|
};
|
||||||
|
let (incremental, telemetry) =
|
||||||
|
layout_dom_with_web_fonts_pass_limit_at_animation_time(
|
||||||
|
&tree,
|
||||||
|
(800.0, 600.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
Some(&mut cache),
|
||||||
|
Some(retained),
|
||||||
|
&[RetainedStyleMutation::Animation { node: animated }],
|
||||||
|
crate::AnimationSample::document(500.0),
|
||||||
|
&mut timeline,
|
||||||
|
);
|
||||||
|
let mut full_timeline = crate::AnimationTimelineState::default();
|
||||||
|
let (full, _) = layout_dom_with_web_fonts_pass_limit_at_animation_time(
|
||||||
|
&tree,
|
||||||
|
(800.0, 600.0),
|
||||||
|
&HashMap::new(),
|
||||||
|
&[],
|
||||||
|
None,
|
||||||
|
Some(&mut cache),
|
||||||
|
None,
|
||||||
|
&[],
|
||||||
|
crate::AnimationSample::document(500.0),
|
||||||
|
&mut full_timeline,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_computed_styles_match("animation retained-vs-full", &incremental, &full);
|
||||||
|
assert_eq!(incremental.rects, full.rects);
|
||||||
|
assert_eq!(telemetry.retained_fallback, 0, "{telemetry:?}");
|
||||||
|
assert!(telemetry.retained_reused >= 4_000, "{telemetry:?}");
|
||||||
|
assert!(telemetry.retained_fresh < 16, "{telemetry:?}");
|
||||||
|
assert!(
|
||||||
|
(incremental.rects[&animated].width - 70.0).abs() < 0.1,
|
||||||
|
"animated geometry did not advance: {:?}",
|
||||||
|
incremental.rects[&animated]
|
||||||
|
);
|
||||||
|
let child = tree.get_element_by_id("child").unwrap();
|
||||||
|
assert_eq!(incremental.styles[&child].color, Some([128, 0, 128, 255]));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn retained_insertions_with_active_container_queries_match_forced_full() {
|
fn retained_insertions_with_active_container_queries_match_forced_full() {
|
||||||
let mut clean = String::new();
|
let mut clean = String::new();
|
||||||
|
|||||||
@@ -1886,6 +1886,21 @@ impl AnimationTimelineState {
|
|||||||
self.waapi.insert(animation.id, animation);
|
self.waapi.insert(animation.id, animation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Exact set of nodes currently targeted by registered Web Animations.
|
||||||
|
/// The render preparation boundary additionally filters disconnected
|
||||||
|
/// targets against the current DOM before producing restyle damage.
|
||||||
|
pub fn waapi_nodes(
|
||||||
|
&self,
|
||||||
|
) -> std::collections::HashSet<obscura_dom::tree::NodeId> {
|
||||||
|
self.waapi.values().map(|animation| animation.node).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// New animation instance epochs must observe a complete cascade. They
|
||||||
|
/// can be created by DOM mutations which have not yet reached style flush.
|
||||||
|
pub fn has_pending_start_candidates(&self) -> bool {
|
||||||
|
!self.start_candidates.is_empty() || !self.subtree_start_candidates.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn cancel_waapi(&mut self, id: u64) -> bool {
|
pub fn cancel_waapi(&mut self, id: u64) -> bool {
|
||||||
self.waapi.remove(&id).is_some()
|
self.waapi.remove(&id).is_some()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2098,7 +2098,14 @@ pub fn prepare_dom_with_retained_styles_with_animation_state(
|
|||||||
animation_sample: crate::AnimationSample,
|
animation_sample: crate::AnimationSample,
|
||||||
animation_timeline: &mut crate::AnimationTimelineState,
|
animation_timeline: &mut crate::AnimationTimelineState,
|
||||||
) -> Option<PreparedRender> {
|
) -> Option<PreparedRender> {
|
||||||
if previous.animation_sample != animation_sample {
|
let sample_changed = previous.animation_sample != animation_sample;
|
||||||
|
let forward_document_sample = sample_changed
|
||||||
|
&& previous.animation_sample.mode == crate::AnimationSampleMode::DocumentTime
|
||||||
|
&& animation_sample.mode == crate::AnimationSampleMode::DocumentTime
|
||||||
|
&& animation_sample.time.milliseconds >= previous.animation_sample.time.milliseconds;
|
||||||
|
if sample_changed
|
||||||
|
&& (!forward_document_sample || animation_timeline.has_pending_start_candidates())
|
||||||
|
{
|
||||||
drop(previous);
|
drop(previous);
|
||||||
return prepare_dom_with_dynamic_fonts_and_stylesheet_cache_with_animation_state(
|
return prepare_dom_with_dynamic_fonts_and_stylesheet_cache_with_animation_state(
|
||||||
tree,
|
tree,
|
||||||
@@ -2111,6 +2118,22 @@ pub fn prepare_dom_with_retained_styles_with_animation_state(
|
|||||||
animation_timeline,
|
animation_timeline,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
let animation_nodes = sample_changed
|
||||||
|
.then(|| retained_animation_restyle_nodes(tree, &previous.layout.styles, animation_timeline))
|
||||||
|
.unwrap_or_default();
|
||||||
|
let animation_mutations;
|
||||||
|
let mutations = if animation_nodes.is_empty() {
|
||||||
|
mutations
|
||||||
|
} else {
|
||||||
|
animation_mutations = mutations
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.chain(animation_nodes.into_iter().map(|node| {
|
||||||
|
crate::dom::RetainedStyleMutation::Animation { node }
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
animation_mutations.as_slice()
|
||||||
|
};
|
||||||
let retained = RetainedStyleMaps {
|
let retained = RetainedStyleMaps {
|
||||||
styles: std::mem::take(&mut previous.layout.styles),
|
styles: std::mem::take(&mut previous.layout.styles),
|
||||||
custom_properties: std::mem::take(&mut previous.layout.custom_properties),
|
custom_properties: std::mem::take(&mut previous.layout.custom_properties),
|
||||||
@@ -2129,6 +2152,26 @@ pub fn prepare_dom_with_retained_styles_with_animation_state(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn retained_animation_restyle_nodes(
|
||||||
|
tree: &DomTree,
|
||||||
|
styles: &HashMap<obscura_dom::tree::NodeId, crate::LayoutStyle>,
|
||||||
|
animation_timeline: &crate::AnimationTimelineState,
|
||||||
|
) -> std::collections::HashSet<obscura_dom::tree::NodeId> {
|
||||||
|
let mut nodes = styles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(node, style)| {
|
||||||
|
(style.animation_name.is_some() && style.animation_has_render_effect)
|
||||||
|
.then_some(*node)
|
||||||
|
})
|
||||||
|
.collect::<std::collections::HashSet<_>>();
|
||||||
|
nodes.extend(animation_timeline.waapi_nodes());
|
||||||
|
nodes.retain(|node| {
|
||||||
|
tree.get_node(*node).is_some()
|
||||||
|
&& (*node == tree.document() || tree.ancestors(*node).contains(&tree.document()))
|
||||||
|
});
|
||||||
|
nodes
|
||||||
|
}
|
||||||
|
|
||||||
fn prepare_dom_with_dynamic_fonts_and_stylesheet_cache_internal(
|
fn prepare_dom_with_dynamic_fonts_and_stylesheet_cache_internal(
|
||||||
tree: &DomTree,
|
tree: &DomTree,
|
||||||
viewport: (f32, f32),
|
viewport: (f32, f32),
|
||||||
@@ -14407,6 +14450,50 @@ mod tests {
|
|||||||
assert!(sticky.blue() > 240 && sticky.red() < 20);
|
assert!(sticky.blue() > 240 && sticky.red() < 20);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn animation_restyle_nodes_deduplicate_waapi_and_filter_disconnected_targets() {
|
||||||
|
let tree = parse_html(
|
||||||
|
"<main><div id=connected></div><div id=detached></div></main>",
|
||||||
|
);
|
||||||
|
let connected = tree.get_element_by_id("connected").unwrap();
|
||||||
|
let detached = tree.get_element_by_id("detached").unwrap();
|
||||||
|
tree.remove_child(detached);
|
||||||
|
let animation = |id, node| crate::WaapiAnimation {
|
||||||
|
id,
|
||||||
|
node,
|
||||||
|
keyframes: vec![crate::WaapiKeyframe {
|
||||||
|
offset: 0.0,
|
||||||
|
opacity: Some(0.0),
|
||||||
|
transform: None,
|
||||||
|
}],
|
||||||
|
timing: crate::AnimationTiming {
|
||||||
|
duration_ms: 1_000.0,
|
||||||
|
..crate::AnimationTiming::default()
|
||||||
|
},
|
||||||
|
easing: None,
|
||||||
|
linear_easing: None,
|
||||||
|
start_time_ms: 0.0,
|
||||||
|
hold_time_ms: None,
|
||||||
|
play_state: crate::WaapiPlayState::Running,
|
||||||
|
};
|
||||||
|
let mut timeline = crate::AnimationTimelineState::default();
|
||||||
|
timeline.register_waapi(animation(1, connected));
|
||||||
|
timeline.register_waapi(animation(2, connected));
|
||||||
|
timeline.register_waapi(animation(3, detached));
|
||||||
|
assert_eq!(
|
||||||
|
timeline.waapi_nodes(),
|
||||||
|
std::collections::HashSet::from([connected, detached]),
|
||||||
|
"the timeline accessor must return the exact deduplicated target set"
|
||||||
|
);
|
||||||
|
|
||||||
|
let nodes = retained_animation_restyle_nodes(&tree, &HashMap::new(), &timeline);
|
||||||
|
assert_eq!(
|
||||||
|
nodes,
|
||||||
|
std::collections::HashSet::from([connected]),
|
||||||
|
"only connected WAAPI targets may enter retained style damage"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn prepared_render_samples_explicit_animation_time_and_reports_live_damage() {
|
fn prepared_render_samples_explicit_animation_time_and_reports_live_damage() {
|
||||||
let tree = parse_html(
|
let tree = parse_html(
|
||||||
|
|||||||
Reference in new issue
Block a user