use oxframe_2d::post::over;
use oxframe_2d::{DrawItem, Primitive, SceneGraph, apply_shade};
use oxframe_core::{Color, Image};
use oxframe_render::{RenderOptions, Renderer, scene_to_pixels};
pub(crate) fn draw_in_parts(
graph: &SceneGraph,
renderer: &mut dyn Renderer,
options: &RenderOptions,
) -> Image {
let items = graph.draw_list();
let view = scene_to_pixels(graph.size(), (options.width.max(1), options.height.max(1)));
let clear = RenderOptions {
background: Color::TRANSPARENT,
..*options
};
let mut frame: Option<Image> = None;
let mut start = 0;
let mut i = 0;
while i < items.len() {
match &items[i].prim {
Primitive::ReadFrame(shade) => {
let mut img = lay(graph, renderer, &items[start..i], options, frame.take());
apply_shade(shade, &mut img, view);
frame = Some(img);
start = i + 1;
}
Primitive::BeginEffect(shade) => {
let end = matching_end(&items, i);
let mut img = lay(graph, renderer, &items[start..i], options, frame.take());
let mut node = renderer.render_list(graph, &items[i + 1..end], &clear);
apply_shade(shade, &mut node, view);
over(&mut img, &node);
frame = Some(img);
start = (end + 1).min(items.len());
i = end;
}
_ => {}
}
i += 1;
}
lay(graph, renderer, &items[start..], options, frame)
}
fn lay(
graph: &SceneGraph,
renderer: &mut dyn Renderer,
part: &[DrawItem],
options: &RenderOptions,
below: Option<Image>,
) -> Image {
let Some(mut img) = below else {
return renderer.render_list(graph, part, options);
};
if part.iter().all(|item| item.prim.is_marker()) {
return img;
}
let clear = RenderOptions {
background: Color::TRANSPARENT,
..*options
};
let top = renderer.render_list(graph, part, &clear);
over(&mut img, &top);
img
}
fn matching_end(items: &[DrawItem], start: usize) -> usize {
let mut depth = 0usize;
for (i, item) in items.iter().enumerate().skip(start) {
match item.prim {
Primitive::BeginEffect(_) => depth += 1,
Primitive::EndEffect => {
depth -= 1;
if depth == 0 {
return i;
}
}
_ => {}
}
}
items.len()
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use oxframe_core::{BoxFuture, Color, Image};
use oxframe_protocol::Meta;
use oxframe_render::CpuRenderer;
use crate::{
Engine, Node, NodeBuilder, Project, Rect, SceneDef, SourceLocation, View, shaders,
};
type SceneFn = fn(View) -> BoxFuture;
fn draw(factory: SceneFn) -> Image {
let project = Project::new("parts")
.size(160, 90)
.fps(10)
.background(Color::BLACK)
.write_meta(false)
.inline_meta(BTreeMap::<String, Meta>::new())
.scene(SceneDef::new(
"main",
SourceLocation {
file: "src/parts.rs",
manifest_dir: env!("CARGO_MANIFEST_DIR"),
},
factory,
));
let mut engine = Engine::new(project).unwrap();
let size = engine.size();
engine.render(0, &mut CpuRenderer::new(), size)
}
fn stripes(view: &View) -> Node {
let card = view.add(crate::Group::new());
for i in 0..8 {
let c = f64::from(i) / 7.0;
card.add(
Rect::new()
.width(20.0)
.height(90.0)
.x(-70.0 + 20.0 * f64::from(i))
.fill(Color::rgb(c as f32, 0.4, (1.0 - c) as f32)),
);
}
*card
}
fn plain(view: View) -> BoxFuture {
Box::pin(async move {
stripes(&view);
view.add(Rect::new().size(16.0).fill(Color::WHITE));
crate::wait_for(0.2).await;
})
}
fn left_lens(view: View) -> BoxFuture {
Box::pin(async move {
stripes(&view);
view.add(
shaders::lens_drops()
.wetness(1.0)
.seed(3)
.width(80.0)
.height(90.0)
.x(-40.0),
);
crate::wait_for(0.2).await;
})
}
fn dry_lens(view: View) -> BoxFuture {
Box::pin(async move {
stripes(&view);
view.add(shaders::lens_drops().wetness(0.0).fullscreen());
view.add(Rect::new().size(16.0).fill(Color::WHITE));
crate::wait_for(0.2).await;
})
}
fn still_ripple(view: View) -> BoxFuture {
Box::pin(async move {
let card = stripes(&view);
card.effect(shaders::ripple().strength(0.0));
view.add(Rect::new().size(16.0).fill(Color::WHITE));
crate::wait_for(0.2).await;
})
}
fn ripple(view: View) -> BoxFuture {
Box::pin(async move {
let card = stripes(&view);
card.effect(shaders::ripple().speed(0.0));
view.add(Rect::new().size(16.0).fill(Color::WHITE));
crate::wait_for(0.2).await;
})
}
fn max_diff(a: &Image, b: &Image) -> u8 {
a.data
.iter()
.zip(&b.data)
.map(|(x, y)| x.abs_diff(*y))
.max()
.unwrap_or(0)
}
fn column(img: &Image, x: u32) -> Vec<u8> {
(0..img.height)
.flat_map(|y| {
let i = ((y * img.width + x) * 4) as usize;
img.data[i..i + 4].to_vec()
})
.collect()
}
#[test]
fn lens_drops_change_only_their_box() {
let lens = draw(left_lens);
let base = draw(|view| {
Box::pin(async move {
stripes(&view);
crate::wait_for(0.2).await;
})
});
for x in 80..160 {
assert_eq!(
column(&lens, x),
column(&base, x),
"column {x} is outside the box"
);
}
assert!((0..80).any(|x| column(&lens, x) != column(&base, x)));
}
#[test]
fn drawing_in_parts_matches_drawing_at_once() {
assert!(max_diff(&draw(dry_lens), &draw(plain)) <= 1);
}
#[test]
fn a_ripple_with_no_strength_draws_the_node_as_it_is() {
assert!(max_diff(&draw(still_ripple), &draw(plain)) <= 1);
}
#[test]
fn a_ripple_moves_the_node_and_not_what_is_drawn_after_it() {
let rippled = draw(ripple);
let base = draw(plain);
assert!(max_diff(&rippled, &base) > 20);
let px = |img: &Image, x: u32, y: u32| {
let i = ((y * img.width + x) * 4) as usize;
img.data[i..i + 4].to_vec()
};
assert_eq!(px(&rippled, 80, 45), px(&base, 80, 45));
}
#[test]
fn effects_match_on_cpu_and_gpu() {
for factory in [left_lens as SceneFn, ripple] {
let project = Project::new("parts")
.size(160, 90)
.fps(10)
.background(Color::BLACK)
.write_meta(false)
.inline_meta(BTreeMap::<String, Meta>::new())
.scene(SceneDef::new(
"main",
SourceLocation {
file: "src/parts.rs",
manifest_dir: env!("CARGO_MANIFEST_DIR"),
},
factory,
));
let mut engine = Engine::new(project).unwrap();
let size = engine.size();
let cpu = engine.render(0, &mut CpuRenderer::new(), size);
let mut gpu = match crate::render::GpuRenderer::new() {
Ok(g) if g.is_software() && std::env::var_os("OXFRAME_GPU_TESTS").is_none() => {
return;
}
Ok(g) => g,
Err(_) => return,
};
let gpu_img = engine.render(0, &mut gpu, size);
let total: u64 = cpu
.data
.iter()
.zip(&gpu_img.data)
.map(|(a, b)| u64::from(a.abs_diff(*b)))
.sum();
let mean = total as f64 / cpu.data.len() as f64;
assert!(mean < 1.0, "CPU and GPU differ by {mean:.2} on average");
}
}
}