use oxideav_scene::{
Background, Canvas, ClipRect, ObjectId, ObjectKind, RasterRenderer, Scene, SceneObject,
SceneRenderer, Shape, Transform,
};
fn pixel(data: &[u8], w: u32, x: u32, y: u32) -> [u8; 4] {
let off = (y as usize * w as usize + x as usize) * 4;
[data[off], data[off + 1], data[off + 2], data[off + 3]]
}
fn top_left_rect(id: u64, x: f32, y: f32, w: f32, h: f32, fill: u32, z: i32) -> SceneObject {
SceneObject {
id: ObjectId::new(id),
kind: ObjectKind::Shape(Shape::Rect {
width: w,
height: h,
fill,
stroke: None,
corner_radius: 0.0,
}),
transform: Transform {
position: (x, y),
anchor: (0.0, 0.0),
..Transform::identity()
},
z_order: z,
..SceneObject::default()
}
}
#[test]
fn composites_background_and_two_shapes() {
let mut scene = Scene {
canvas: Canvas::raster(64, 64),
background: Background::Solid(0x202020FF),
..Scene::default()
};
scene
.objects
.push(top_left_rect(1, 0.0, 0.0, 40.0, 40.0, 0x0000FFFF, 0));
scene
.objects
.push(top_left_rect(2, 10.0, 10.0, 20.0, 20.0, 0xFF0000FF, 1));
let mut r = RasterRenderer::new();
r.prepare(&scene).unwrap();
let frame = r.render_at(&scene, 0).unwrap().video.unwrap();
let data = &frame.planes[0].data;
assert_eq!(data.len(), 64 * 64 * 4);
assert_eq!(pixel(data, 64, 60, 60), [0x20, 0x20, 0x20, 0xFF]);
let blue = pixel(data, 64, 35, 5);
assert!(blue[2] > 200 && blue[0] < 60, "expected blue: {blue:?}");
let red = pixel(data, 64, 15, 15);
assert!(red[0] > 200 && red[2] < 60, "expected red on top: {red:?}");
}
#[test]
fn drives_a_finite_scene_to_frames() {
use oxideav_core::Rational;
use oxideav_scene::{RenderedSource, SceneSource};
let mut scene = Scene {
canvas: Canvas::raster(16, 16),
duration: oxideav_scene::SceneDuration::Finite(100),
framerate: Rational::new(30, 1),
background: Background::Solid(0x0000FFFF),
..Scene::default()
};
scene
.objects
.push(top_left_rect(1, 0.0, 0.0, 16.0, 16.0, 0xFFFFFFFF, 0));
let mut src = RenderedSource::new(scene, RasterRenderer::new());
let first = src
.pull()
.expect("pull should succeed")
.expect("source should yield a first frame");
let frame = first.video.expect("frame should carry video");
assert_eq!(frame.planes[0].data.len(), 16 * 16 * 4);
let px = pixel(&frame.planes[0].data, 16, 8, 8);
assert_eq!(px, [0xFF, 0xFF, 0xFF, 0xFF]);
}
#[test]
fn group_composes_children_under_group_transform() {
let parent = SceneObject {
id: ObjectId::new(100),
kind: ObjectKind::Group(vec![ObjectId::new(1), ObjectId::new(2)]),
transform: Transform {
position: (20.0, 20.0),
anchor: (0.0, 0.0),
..Transform::identity()
},
..SceneObject::default()
};
let child1 = top_left_rect(1, 0.0, 0.0, 8.0, 8.0, 0xFF0000FF, 0);
let child2 = top_left_rect(2, 20.0, 0.0, 8.0, 8.0, 0x00FF00FF, 0);
let mut scene = Scene {
canvas: Canvas::raster(64, 64),
background: Background::Transparent,
..Scene::default()
};
scene.objects.push(parent);
scene.objects.push(child1);
scene.objects.push(child2);
let mut r = RasterRenderer::new();
let frame = r.render_at(&scene, 0).unwrap().video.unwrap();
let data = &frame.planes[0].data;
let red = pixel(data, 64, 23, 23);
assert!(red[0] > 200 && red[1] < 60, "expected red: {red:?}");
let green = pixel(data, 64, 43, 23);
assert!(green[1] > 200 && green[0] < 60, "expected green: {green:?}");
assert_eq!(pixel(data, 64, 4, 4)[3], 0, "child must not double-paint");
}
#[test]
fn group_opacity_multiplies_through_children() {
let mut parent = SceneObject {
id: ObjectId::new(100),
kind: ObjectKind::Group(vec![ObjectId::new(1)]),
..SceneObject::default()
};
parent.opacity = 0.5;
let child = top_left_rect(1, 0.0, 0.0, 16.0, 16.0, 0xFFFFFFFF, 0);
let mut scene = Scene {
canvas: Canvas::raster(16, 16),
background: Background::Solid(0x000000FF),
..Scene::default()
};
scene.objects.push(parent);
scene.objects.push(child);
let mut r = RasterRenderer::new();
let frame = r.render_at(&scene, 0).unwrap().video.unwrap();
let px = pixel(&frame.planes[0].data, 16, 8, 8);
assert!((90..=165).contains(&px[0]), "expected mid-grey, got {px:?}");
}
#[test]
fn group_with_missing_child_id_does_not_panic() {
let parent = SceneObject {
id: ObjectId::new(100),
kind: ObjectKind::Group(vec![ObjectId::new(42), ObjectId::new(1)]),
..SceneObject::default()
};
let child = top_left_rect(1, 0.0, 0.0, 8.0, 8.0, 0xFFFFFFFF, 0);
let mut scene = Scene {
canvas: Canvas::raster(16, 16),
background: Background::Transparent,
..Scene::default()
};
scene.objects.push(parent);
scene.objects.push(child);
let mut r = RasterRenderer::new();
let frame = r.render_at(&scene, 0).unwrap().video.unwrap();
assert_eq!(pixel(&frame.planes[0].data, 16, 4, 4)[3], 0xFF);
}
#[test]
fn group_cycle_terminates() {
let g_a = SceneObject {
id: ObjectId::new(100),
kind: ObjectKind::Group(vec![ObjectId::new(101)]),
..SceneObject::default()
};
let g_b = SceneObject {
id: ObjectId::new(101),
kind: ObjectKind::Group(vec![ObjectId::new(100)]),
..SceneObject::default()
};
let mut scene = Scene {
canvas: Canvas::raster(8, 8),
background: Background::Transparent,
..Scene::default()
};
scene.objects.push(g_a);
scene.objects.push(g_b);
let mut r = RasterRenderer::new();
let frame = r.render_at(&scene, 0).unwrap().video.unwrap();
assert_eq!(frame.planes[0].data.len(), 8 * 8 * 4);
}
#[test]
fn shape_path_svg_renders_as_filled_polygon() {
let path_obj = SceneObject {
id: ObjectId::new(1),
kind: ObjectKind::Shape(Shape::Path {
data: "M16,4 L4,28 L28,28 Z".to_string(),
fill: 0xFFFFFFFF,
stroke: None,
}),
transform: Transform {
position: (0.0, 0.0),
anchor: (0.0, 0.0),
..Transform::identity()
},
..SceneObject::default()
};
let mut scene = Scene {
canvas: Canvas::raster(32, 32),
background: Background::Solid(0x000000FF),
..Scene::default()
};
scene.objects.push(path_obj);
let mut r = RasterRenderer::new();
let frame = r.render_at(&scene, 0).unwrap().video.unwrap();
let data = &frame.planes[0].data;
let inside = pixel(data, 32, 16, 22);
assert!(
inside[0] > 200 && inside[1] > 200 && inside[2] > 200,
"triangle interior should be white: {inside:?}"
);
let outside = pixel(data, 32, 1, 1);
assert_eq!(outside, [0x00, 0x00, 0x00, 0xFF]);
}
#[test]
fn shape_path_with_arc_skips_without_error() {
let arc_obj = SceneObject {
id: ObjectId::new(1),
kind: ObjectKind::Shape(Shape::Path {
data: "M0,0 A 5 5 0 0 0 10 10".to_string(),
fill: 0xFF0000FF,
stroke: None,
}),
..SceneObject::default()
};
let mut scene = Scene {
canvas: Canvas::raster(16, 16),
background: Background::Transparent,
..Scene::default()
};
scene.objects.push(arc_obj);
let mut r = RasterRenderer::new();
let frame = r.render_at(&scene, 0).unwrap().video.unwrap();
let lit = frame.planes[0]
.data
.chunks_exact(4)
.filter(|p| p[3] != 0)
.count();
assert_eq!(lit, 0);
}
#[test]
fn group_clip_intersects_with_child_clip_region() {
let parent = SceneObject {
id: ObjectId::new(100),
kind: ObjectKind::Group(vec![ObjectId::new(1)]),
clip: Some(ClipRect {
x: 0.0,
y: 0.0,
width: 8.0,
height: 8.0,
}),
..SceneObject::default()
};
let child = top_left_rect(1, 0.0, 0.0, 16.0, 16.0, 0xFFFFFFFF, 0);
let mut scene = Scene {
canvas: Canvas::raster(16, 16),
background: Background::Transparent,
..Scene::default()
};
scene.objects.push(parent);
scene.objects.push(child);
let mut r = RasterRenderer::new();
let frame = r.render_at(&scene, 0).unwrap().video.unwrap();
let data = &frame.planes[0].data;
assert_eq!(pixel(data, 16, 4, 4)[3], 0xFF);
assert_eq!(pixel(data, 16, 12, 12)[3], 0);
}
#[test]
fn animated_opacity_changes_rendered_coverage() {
use oxideav_scene::{AnimatedProperty, Animation, Easing, Keyframe, KeyframeValue, Repeat};
let mut scene = Scene {
canvas: Canvas::raster(16, 16),
background: Background::Solid(0x000000FF),
..Scene::default()
};
let fade = Animation::new(
AnimatedProperty::Opacity,
vec![
Keyframe {
time: 0,
value: KeyframeValue::Scalar(0.0),
easing: None,
},
Keyframe {
time: 100,
value: KeyframeValue::Scalar(1.0),
easing: None,
},
],
Easing::Linear,
Repeat::Once,
);
let mut obj = top_left_rect(1, 0.0, 0.0, 16.0, 16.0, 0xFFFFFFFF, 0);
obj.animations = vec![fade];
scene.objects.push(obj);
let mut r = RasterRenderer::new();
let early = r.render_at(&scene, 0).unwrap().video.unwrap();
let late = r.render_at(&scene, 100).unwrap().video.unwrap();
let e = pixel(&early.planes[0].data, 16, 8, 8)[0] as i32;
let l = pixel(&late.planes[0].data, 16, 8, 8)[0] as i32;
assert!(
l > e + 150,
"opacity ramp should brighten: early={e} late={l}"
);
}