Retain clean styles across animation frames
This commit is contained in:
1 parent
fd87ddd0b9
commit
47e9aa096b
5 files changed
+345
-14
No files matched your search
@@ -3427,18 +3427,22 @@ pub(crate) fn ensure_prepared_render(
|
||||
let base_url = document_base_url(state);
|
||||
let viewport = state.viewport;
|
||||
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.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() {
|
||||
state
|
||||
.animation_timeline
|
||||
.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 prepared = {
|
||||
let dom = state.dom.as_ref()?;
|
||||
@@ -3520,9 +3524,15 @@ pub(crate) fn sample_live_document_animations(state: &mut ObscuraState) {
|
||||
state.animation_sample = sample;
|
||||
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.prepared_render = None;
|
||||
state.pending_style_mutations.clear();
|
||||
if !forward_document_sample {
|
||||
state.prepared_render = None;
|
||||
state.pending_style_mutations.clear();
|
||||
}
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
|
||||
|
||||
@@ -471,9 +471,11 @@ impl ObscuraJsRuntime {
|
||||
}
|
||||
let mut state = self.state.borrow_mut();
|
||||
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
|
||||
&& 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| {
|
||||
prepared.advance_inactive_animation_sample_time(sample.time)
|
||||
})
|
||||
@@ -482,8 +484,10 @@ impl ObscuraJsRuntime {
|
||||
return true;
|
||||
}
|
||||
state.animation_sample = sample;
|
||||
state.prepared_render = None;
|
||||
state.pending_style_mutations.clear();
|
||||
if !forward_document_sample {
|
||||
state.prepared_render = None;
|
||||
state.pending_style_mutations.clear();
|
||||
}
|
||||
state.resolved_scroll = None;
|
||||
}
|
||||
true
|
||||
@@ -6879,6 +6883,100 @@ mod tests {
|
||||
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")]
|
||||
#[test]
|
||||
fn completed_animation_retains_forward_frame_but_backward_seek_rebuilds() {
|
||||
|
||||
Reference in new issue
Block a user