use rustmotion_core::css::CssStyle;
use rustmotion_core::engine::animator::{resolve_props_for_effects, AnimatedProperties};
use rustmotion_core::engine::box_tree::NodeId;
use rustmotion_core::engine::layout_pass::BoxLayout;
use rustmotion_core::engine::paint_pass::{PaintDispatcher, PaintFrame};
use rustmotion_core::traits::PaintCtx;
use skia_safe::Canvas;
use crate::{ChildComponent, Component};
pub struct LegacyPaintDispatcher<'a> {
components: &'a [Option<&'a ChildComponent>],
stagger_delays: &'a [f64],
time_params: &'a [(f64, f64)],
}
impl<'a> LegacyPaintDispatcher<'a> {
pub fn new(components: &'a [Option<&'a ChildComponent>]) -> Self {
Self {
components,
stagger_delays: &[],
time_params: &[],
}
}
pub fn for_scene(built: &'a crate::box_builder::BuiltScene<'a>) -> Self {
Self {
components: &built.components,
stagger_delays: &built.stagger_delays,
time_params: &built.time_params,
}
}
fn lookup(&self, id: NodeId) -> Option<&'a ChildComponent> {
let idx = id as usize;
self.components.get(idx).copied().flatten()
}
}
impl<'a> PaintDispatcher for LegacyPaintDispatcher<'a> {
fn dispatch(
&self,
canvas: &Canvas,
payload: &(dyn std::any::Any + Send + Sync),
css: &CssStyle,
layout: &BoxLayout,
frame: &PaintFrame,
) {
let Some(node_id) = payload.downcast_ref::<NodeId>() else {
return;
};
let Some(child) = self.lookup(*node_id) else {
return;
};
if is_container(&child.component) {
return;
}
let stagger_delay = self
.stagger_delays
.get(*node_id as usize)
.copied()
.unwrap_or(0.0);
let (t_scale, t_shift) = self
.time_params
.get(*node_id as usize)
.copied()
.unwrap_or((1.0, 0.0));
let local_time = frame.time * t_scale + t_shift;
let props = match crate::box_builder::effective_effects(
&child.component,
stagger_delay,
local_time,
) {
Some(effects) => resolve_props_for_effects(&effects, local_time, frame.scene_duration),
None => AnimatedProperties::default(),
};
if props.opacity <= 0.0 {
return;
}
let cascaded_component = child.component.with_cascaded_style(css);
let cascaded = cascaded_component.as_ref().unwrap_or(&child.component);
let repainted = cascaded.with_resolved_background(css);
let effective_component = repainted.as_ref().unwrap_or(cascaded);
let Some(painter) = effective_component.as_painter() else {
return;
};
canvas.save();
let (cx, cy, cw, ch) = layout.content_box();
canvas.translate((cx, cy));
let local = BoxLayout {
x: 0.0,
y: 0.0,
width: cw,
height: ch,
..Default::default()
};
let paint_ctx = PaintCtx {
time: local_time,
scenario_time: frame.scenario_time,
scene_duration: frame.scene_duration,
frame_index: frame.frame_index,
fps: frame.fps,
video_width: frame.video_width,
video_height: frame.video_height,
stagger_offset: stagger_delay,
};
painter.paint_content(canvas, &local, &props, &paint_ctx);
canvas.restore();
}
}
fn is_container(c: &Component) -> bool {
matches!(c, Component::Container(_))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::box_builder::build_scene;
use crate::shape::Shape;
use crate::PositionMode;
use rustmotion_core::css::style::{CssStyle, Size as CSize};
use rustmotion_core::css::taffy_bridge::ConversionContext;
use rustmotion_core::css::units::LengthPercentage as CLP;
use rustmotion_core::engine::box_tree::BoxKind;
use rustmotion_core::engine::layout_pass::run_layout;
use rustmotion_core::schema::ShapeType;
use std::sync::Arc;
fn shape_child(w: f32, h: f32, x: f32, y: f32) -> ChildComponent {
ChildComponent {
id: None,
component: Component::Shape(Shape {
shape: ShapeType::Rect,
text: None,
timing: Default::default(),
style: CssStyle {
width: Some(CSize::Length(CLP::Px(w))),
height: Some(CSize::Length(CLP::Px(h))),
..Default::default()
},
timeline: Vec::new(),
stagger: None,
fill: Some(rustmotion_core::schema::Fill::Solid("#ff0000".into())),
stroke: None,
draw_start: None,
path_morph: None,
}),
position: Some(PositionMode::Absolute { x, y }),
x: None,
y: None,
z_index: None,
bleed: false,
}
}
#[test]
fn leaf_painter_content_is_inset_by_padding() {
use rustmotion_core::css::style::Edges;
let mut scene = vec![shape_child(100.0, 80.0, 0.0, 0.0)];
if let Component::Shape(s) = &mut scene[0].component {
s.style.padding = Some(Edges::Uniform(CLP::Px(20.0)));
}
let built = build_scene(&scene, (200.0, 200.0));
let layout = run_layout(&built.root, (200.0, 200.0), &ConversionContext::default());
let mut surface =
skia_safe::surfaces::raster_n32_premul((200, 200)).expect("raster surface");
let canvas = surface.canvas();
let dispatcher = LegacyPaintDispatcher::new(&built.components);
let frame = PaintFrame {
light: Default::default(),
time: 0.0,
scenario_time: 0.0,
frame_index: 0,
fps: 30,
video_width: 200,
video_height: 200,
scene_duration: 1.0,
camera: None,
};
rustmotion_core::engine::paint_pass::paint_tree(
canvas,
&built.root,
&layout,
&frame,
&dispatcher,
);
let snapshot = surface.image_snapshot();
let info = skia_safe::ImageInfo::new(
(1, 1),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Premul,
None,
);
let read = |x: i32, y: i32| -> [u8; 4] {
let mut buf = [0u8; 4];
assert!(snapshot.read_pixels(
&info,
&mut buf,
4,
skia_safe::IPoint::new(x, y),
skia_safe::image::CachingHint::Disallow,
));
buf
};
let padding_zone = read(5, 5);
assert!(
!(padding_zone[0] > 200 && padding_zone[1] < 50 && padding_zone[2] < 50),
"padding ring must not be painted by the leaf's own fill, got {:?}",
padding_zone
);
let content_zone = read(50, 40);
assert!(
content_zone[0] > 200 && content_zone[1] < 50 && content_zone[2] < 50,
"content box must still be painted red, got {:?}",
content_zone
);
}
#[test]
fn dispatch_runs_paint_content_on_leaf() {
let scene = vec![shape_child(50.0, 30.0, 10.0, 20.0)];
let built = build_scene(&scene, (200.0, 200.0));
let layout = run_layout(&built.root, (200.0, 200.0), &ConversionContext::default());
assert!(built.components[0].is_none());
assert!(built.components[1].is_some());
let mut surface =
skia_safe::surfaces::raster_n32_premul((200, 200)).expect("raster surface");
let canvas = surface.canvas();
let dispatcher = LegacyPaintDispatcher::new(&built.components);
let frame = PaintFrame {
light: Default::default(),
time: 0.0,
scenario_time: 0.0,
frame_index: 0,
fps: 30,
video_width: 200,
video_height: 200,
scene_duration: 1.0,
camera: None,
};
rustmotion_core::engine::paint_pass::paint_tree(
canvas,
&built.root,
&layout,
&frame,
&dispatcher,
);
let snapshot = surface.image_snapshot();
let mut buf = [0u8; 4];
let info = skia_safe::ImageInfo::new(
(1, 1),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Premul,
None,
);
let read_ok = snapshot.read_pixels(
&info,
&mut buf,
4,
skia_safe::IPoint::new(35, 35),
skia_safe::image::CachingHint::Disallow,
);
assert!(read_ok, "pixel read should succeed");
assert!(buf[0] > 200, "expected red, got rgba {:?}", buf);
assert!(buf[1] < 50, "green should be low, got rgba {:?}", buf);
assert!(buf[2] < 50, "blue should be low, got rgba {:?}", buf);
}
#[test]
fn card_background_painted_with_red_shape_inside() {
use crate::container::ContainerComponent;
use rustmotion_core::css::style::{Background, Color};
let red_shape = ChildComponent {
id: None,
component: Component::Shape(Shape {
shape: ShapeType::Rect,
text: None,
timing: Default::default(),
style: CssStyle {
width: Some(CSize::Length(CLP::Px(30.0))),
height: Some(CSize::Length(CLP::Px(20.0))),
..Default::default()
},
timeline: Vec::new(),
stagger: None,
fill: Some(rustmotion_core::schema::Fill::Solid("#ff0000".into())),
stroke: None,
draw_start: None,
path_morph: None,
}),
position: Some(PositionMode::Absolute { x: 10.0, y: 10.0 }),
x: None,
y: None,
z_index: None,
bleed: false,
};
let card = ChildComponent {
id: None,
component: Component::Container(ContainerComponent {
children: vec![red_shape],
timing: Default::default(),
style: CssStyle {
width: Some(CSize::Length(CLP::Px(100.0))),
height: Some(CSize::Length(CLP::Px(80.0))),
background: Some(Background::Color(Color::String("#00ff00".into()))),
..Default::default()
},
timeline: Vec::new(),
stagger: None,
time_scale: None,
time_offset: None,
}),
position: Some(PositionMode::Absolute { x: 40.0, y: 30.0 }),
x: None,
y: None,
z_index: None,
bleed: false,
};
let scene = vec![card];
let built = build_scene(&scene, (200.0, 200.0));
let layout = run_layout(&built.root, (200.0, 200.0), &ConversionContext::default());
let mut surface =
skia_safe::surfaces::raster_n32_premul((200, 200)).expect("raster surface");
let canvas = surface.canvas();
let dispatcher = LegacyPaintDispatcher::new(&built.components);
let frame = PaintFrame {
light: Default::default(),
time: 0.0,
scenario_time: 0.0,
frame_index: 0,
fps: 30,
video_width: 200,
video_height: 200,
scene_duration: 1.0,
camera: None,
};
rustmotion_core::engine::paint_pass::paint_tree(
canvas,
&built.root,
&layout,
&frame,
&dispatcher,
);
let snapshot = surface.image_snapshot();
let info = skia_safe::ImageInfo::new(
(1, 1),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Premul,
None,
);
let read = |x: i32, y: i32| -> [u8; 4] {
let mut buf = [0u8; 4];
assert!(snapshot.read_pixels(
&info,
&mut buf,
4,
skia_safe::IPoint::new(x, y),
skia_safe::image::CachingHint::Disallow,
));
buf
};
let bg = read(130, 100);
assert!(bg[1] > 200, "expected green card bg, got {:?}", bg);
assert!(bg[0] < 50, "red should be low at bg, got {:?}", bg);
let fg = read(65, 50);
assert!(fg[0] > 200, "expected red shape, got {:?}", fg);
assert!(fg[1] < 50, "green should be low at shape, got {:?}", fg);
}
#[test]
fn fade_in_preset_drives_alpha_through_dispatcher() {
use crate::shape::Shape;
use rustmotion_core::schema::{AnimationEffect, AnimationTiming, ShapeType};
let make_scene = || {
let shape = ChildComponent {
id: None,
component: Component::Shape(Shape {
shape: ShapeType::Rect,
text: None,
timing: Default::default(),
style: CssStyle {
width: Some(CSize::Length(CLP::Px(100.0))),
height: Some(CSize::Length(CLP::Px(100.0))),
animation: vec![AnimationEffect::FadeIn(AnimationTiming {
duration: 0.5,
..Default::default()
})],
..Default::default()
},
timeline: Vec::new(),
stagger: None,
fill: Some(rustmotion_core::schema::Fill::Solid("#ff0000".into())),
stroke: None,
draw_start: None,
path_morph: None,
}),
position: Some(PositionMode::Absolute { x: 0.0, y: 0.0 }),
x: None,
y: None,
z_index: None,
bleed: false,
};
vec![shape]
};
let sample_red_at = |time: f64| -> u8 {
let scene = make_scene();
let built = crate::box_builder::build_scene_with_anim(
&scene,
(200.0, 200.0),
crate::box_builder::BuildAnimationCtx {
time,
scenario_time: time,
scene_duration: 1.0,
fps: 30,
},
);
let layout = run_layout(&built.root, (200.0, 200.0), &ConversionContext::default());
let mut surface =
skia_safe::surfaces::raster_n32_premul((200, 200)).expect("raster surface");
let canvas = surface.canvas();
canvas.clear(skia_safe::Color::BLACK);
let dispatcher = LegacyPaintDispatcher::new(&built.components);
let frame = PaintFrame {
light: Default::default(),
time,
scenario_time: time,
frame_index: 0,
fps: 30,
video_width: 200,
video_height: 200,
scene_duration: 1.0,
camera: None,
};
rustmotion_core::engine::paint_pass::paint_tree(
canvas,
&built.root,
&layout,
&frame,
&dispatcher,
);
let snap = surface.image_snapshot();
let info = skia_safe::ImageInfo::new(
(1, 1),
skia_safe::ColorType::RGBA8888,
skia_safe::AlphaType::Premul,
None,
);
let mut buf = [0u8; 4];
assert!(snap.read_pixels(
&info,
&mut buf,
4,
skia_safe::IPoint::new(50, 50),
skia_safe::image::CachingHint::Disallow,
));
buf[0]
};
let early = sample_red_at(0.05);
let late = sample_red_at(0.5);
assert!(
late > early + 50,
"FadeIn should produce a clearly higher red at t=0.5 than at t=0.05 \
(early={}, late={})",
early,
late,
);
assert!(
late > 200,
"at t=duration the shape should be ~fully opaque red, got {}",
late
);
assert!(
early < 150,
"at t=0.05 the shape should be mostly transparent, got {}",
early
);
}
#[test]
fn dispatch_skips_unknown_payloads() {
let dispatcher = LegacyPaintDispatcher::new(&[]);
let mut surface = skia_safe::surfaces::raster_n32_premul((10, 10)).unwrap();
let canvas = surface.canvas();
let css = rustmotion_core::css::CssStyle::default();
let layout = BoxLayout {
x: 0.0,
y: 0.0,
width: 10.0,
height: 10.0,
..Default::default()
};
let frame = PaintFrame {
light: Default::default(),
time: 0.0,
scenario_time: 0.0,
frame_index: 0,
fps: 30,
video_width: 10,
video_height: 10,
scene_duration: 1.0,
camera: None,
};
let bogus: Arc<dyn std::any::Any + Send + Sync> = Arc::new(42i64);
dispatcher.dispatch(canvas, bogus.as_ref(), &css, &layout, &frame);
let _ = BoxKind::Container;
}
}