Animation: update image active frame when update the rendering (#36286)

When no CSS animation exist, register timer for image animation, update
animated image active image frame as part of update_the_rendering, mark
node as dirty if the corresponding image need update. Added unit test to
test ImageAnimationState.

Part of https://github.com/servo/servo/issues/36057, the last step to
let the Animated Image "Move".

Testing: Introduced new WPT RefTest for animated image, but fail because
of https://github.com/servo/servo/issues/36931. New unit test for
`ImageAnimationState`.
Fixes: https://github.com/servo/servo/issues/22903
https://github.com/servo/servo/issues/36057

[Try](https://github.com/rayguo17/servo/actions/runs/14724729664)

---------

Signed-off-by: rayguo17 <rayguo17@gmail.com>
This commit is contained in:
TIN TUN AUNG 2025-05-23 11:13:35 +08:00 committed by GitHub
parent 2353c0089f
commit 23ce7b31ac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 313 additions and 8 deletions

View file

@ -507,21 +507,116 @@ pub fn node_id_from_scroll_id(id: usize) -> Option<usize> {
#[derive(Clone, Debug, MallocSizeOf)]
pub struct ImageAnimationState {
#[ignore_malloc_size_of = "Arc is hard"]
image: Arc<Image>,
active_frame: usize,
pub image: Arc<Image>,
pub active_frame: usize,
last_update_time: f64,
}
impl ImageAnimationState {
pub fn new(image: Arc<Image>) -> Self {
pub fn new(image: Arc<Image>, last_update_time: f64) -> Self {
Self {
image,
active_frame: 0,
last_update_time: 0.,
last_update_time,
}
}
pub fn image_key(&self) -> Option<ImageKey> {
self.image.id
}
pub fn time_to_next_frame(&self, now: f64) -> f64 {
let frame_delay = self
.image
.frames
.get(self.active_frame)
.expect("Image frame should always be valid")
.delay
.map_or(0., |delay| delay.as_secs_f64());
(frame_delay - now + self.last_update_time).max(0.0)
}
/// check whether image active frame need to be updated given current time,
/// return true if there are image that need to be updated.
/// false otherwise.
pub fn update_frame_for_animation_timeline_value(&mut self, now: f64) -> bool {
if self.image.frames.len() <= 1 {
return false;
}
let image = &self.image;
let time_interval_since_last_update = now - self.last_update_time;
let mut remain_time_interval = time_interval_since_last_update -
image
.frames
.get(self.active_frame)
.unwrap()
.delay
.unwrap()
.as_secs_f64();
let mut next_active_frame_id = self.active_frame;
while remain_time_interval > 0.0 {
next_active_frame_id = (next_active_frame_id + 1) % image.frames.len();
remain_time_interval -= image
.frames
.get(next_active_frame_id)
.unwrap()
.delay
.unwrap()
.as_secs_f64();
}
if self.active_frame == next_active_frame_id {
return false;
}
self.active_frame = next_active_frame_id;
self.last_update_time = now;
true
}
}
#[cfg(test)]
mod test {
use std::sync::Arc;
use std::time::Duration;
use ipc_channel::ipc::IpcSharedMemory;
use pixels::{CorsStatus, Image, ImageFrame, PixelFormat};
use crate::ImageAnimationState;
#[test]
fn test() {
let image_frames: Vec<ImageFrame> = std::iter::repeat_with(|| ImageFrame {
delay: Some(Duration::from_millis(100)),
byte_range: 0..1,
width: 100,
height: 100,
})
.take(10)
.collect();
let image = Image {
width: 100,
height: 100,
format: PixelFormat::BGRA8,
id: None,
bytes: IpcSharedMemory::from_byte(1, 1),
frames: image_frames,
cors_status: CorsStatus::Unsafe,
};
let mut image_animation_state = ImageAnimationState::new(Arc::new(image), 0.0);
assert_eq!(image_animation_state.active_frame, 0);
assert_eq!(image_animation_state.last_update_time, 0.0);
assert_eq!(
image_animation_state.update_frame_for_animation_timeline_value(0.101),
true
);
assert_eq!(image_animation_state.active_frame, 1);
assert_eq!(image_animation_state.last_update_time, 0.101);
assert_eq!(
image_animation_state.update_frame_for_animation_timeline_value(0.116),
false
);
assert_eq!(image_animation_state.active_frame, 1);
assert_eq!(image_animation_state.last_update_time, 0.101);
}
}