use super::*;
use crate::draw_ctx::DrawCtx;
use crate::TransAffine;
fn is_blue(pixel: [u8; 4]) -> bool {
pixel[2] > 200 && pixel[0] < 50 && pixel[1] < 50
}
fn clipped_circle_fb() -> Framebuffer {
let mut fb = Framebuffer::new(64, 64);
{
let mut gfx = GfxCtx::new(&mut fb);
let ctx: &mut dyn DrawCtx = &mut gfx;
assert!(ctx.supports_clip_path());
ctx.clear(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 64.0);
ctx.fill();
ctx.restore();
}
fb
}
#[test]
fn clip_path_masks_fill_to_the_circle() {
let fb = clipped_circle_fb();
assert!(is_blue(sample(&fb, 32, 32)), "centre must be blue");
assert!(is_red(sample(&fb, 1, 1)), "corner must stay red");
assert!(
is_red(sample(&fb, 52, 32)),
"outside the circle must be red"
);
}
#[test]
fn clip_is_released_after_restore() {
let mut fb = Framebuffer::new(64, 64);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 64.0);
ctx.fill();
ctx.restore();
ctx.set_fill_color(Color::rgba(0.0, 1.0, 0.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 64.0);
ctx.fill();
}
let corner = sample(&fb, 1, 1);
assert!(
corner[1] > 200 && corner[0] < 50,
"corner must be green after restore; got {corner:?}"
);
}
#[test]
fn clip_path_respects_translate_and_rotate() {
let mut fb = Framebuffer::new(64, 64);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.save();
ctx.translate(32.0, 32.0);
ctx.rotate(std::f64::consts::FRAC_PI_4);
ctx.begin_path();
ctx.rect(-10.0, -10.0, 20.0, 20.0);
ctx.clip_path();
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(-100.0, -100.0, 200.0, 200.0);
ctx.fill();
ctx.restore();
}
assert!(is_blue(sample(&fb, 32, 32)), "centre must be blue");
assert!(is_blue(sample(&fb, 32, 43)), "on-axis point must be blue");
assert!(
is_red(sample(&fb, 44, 44)),
"the rotated square's diagonal corner must stay red"
);
}
#[test]
fn nested_clip_paths_intersect() {
let mut fb = Framebuffer::new(64, 64);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.save();
ctx.begin_path();
ctx.rect(0.0, 0.0, 32.0, 64.0); ctx.clip_path();
ctx.save();
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 32.0); ctx.clip_path();
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 64.0);
ctx.fill();
ctx.restore();
ctx.restore();
}
assert!(is_blue(sample(&fb, 16, 16)), "bottom-left must be blue");
assert!(is_red(sample(&fb, 48, 16)), "bottom-right must be red");
assert!(is_red(sample(&fb, 16, 48)), "top-left must be red");
assert!(is_red(sample(&fb, 48, 48)), "top-right must be red");
}
#[test]
fn empty_path_clip_draws_nothing() {
let mut fb = Framebuffer::new(32, 32);
{
let mut ctx = GfxCtx::new(&mut fb);
ctx.clear(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.save();
ctx.begin_path();
ctx.clip_path();
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 32.0, 32.0);
ctx.fill();
ctx.restore();
}
assert!(is_red(sample(&fb, 16, 16)), "empty clip must draw nothing");
}
fn is_green(pixel: [u8; 4]) -> bool {
pixel[1] > 200 && pixel[0] < 60 && pixel[2] < 60
}
#[test]
fn clip_path_keeps_the_current_path_for_a_later_stroke() {
let mut fb = Framebuffer::new(64, 64);
{
let mut gfx = GfxCtx::new(&mut fb);
let ctx: &mut dyn DrawCtx = &mut gfx;
ctx.clear(Color::rgba(1.0, 1.0, 1.0, 1.0));
ctx.set_fill_color(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.fill();
ctx.save();
ctx.clip_path();
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 64.0);
ctx.fill();
ctx.restore();
ctx.set_stroke_color(Color::rgba(0.0, 1.0, 0.0, 1.0));
ctx.set_line_width(6.0);
ctx.stroke();
}
let edge = sample(&fb, 48, 32);
assert!(
is_green(edge),
"stroke must follow the clipping circle; got {edge:?}"
);
assert!(
is_blue(sample(&fb, 32, 32)),
"centre must stay blue; got {:?}",
sample(&fb, 32, 32)
);
}
#[test]
fn balanced_save_restore_inside_clip_does_not_pop_it() {
let mut fb = Framebuffer::new(64, 64);
{
let mut gfx = GfxCtx::new(&mut fb);
let ctx: &mut dyn DrawCtx = &mut gfx;
ctx.clear(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
ctx.save();
ctx.set_fill_color(Color::rgba(0.0, 1.0, 0.0, 1.0));
ctx.restore();
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 64.0);
ctx.fill();
ctx.restore();
}
assert!(is_blue(sample(&fb, 32, 32)), "centre must be blue");
assert!(
is_red(sample(&fb, 1, 1)),
"the clip must still be active after the inner restore; got {:?}",
sample(&fb, 1, 1)
);
}
#[test]
fn clip_path_inside_a_compositing_layer() {
let mut fb = Framebuffer::new(64, 64);
{
let mut gfx = GfxCtx::new(&mut fb);
let ctx: &mut dyn DrawCtx = &mut gfx;
ctx.clear(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.push_layer(64.0, 64.0);
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 64.0);
ctx.fill();
ctx.restore();
ctx.pop_layer();
}
assert!(is_blue(sample(&fb, 32, 32)), "centre must be blue");
assert!(is_red(sample(&fb, 1, 1)), "corner must stay red");
}
#[test]
fn compositing_layer_inside_a_clip_path() {
let mut fb = Framebuffer::new(64, 64);
{
let mut gfx = GfxCtx::new(&mut fb);
let ctx: &mut dyn DrawCtx = &mut gfx;
ctx.clear(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.save();
ctx.begin_path();
ctx.circle(32.0, 32.0, 16.0);
ctx.clip_path();
ctx.push_layer(64.0, 64.0);
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 64.0);
ctx.fill();
ctx.pop_layer();
ctx.restore();
}
assert!(is_blue(sample(&fb, 32, 32)), "centre must be blue");
assert!(is_red(sample(&fb, 1, 1)), "corner must stay red");
}
#[test]
fn clip_path_under_a_scaled_ctm() {
let mut fb = Framebuffer::new(64, 64);
{
let mut gfx = GfxCtx::new(&mut fb);
let ctx: &mut dyn DrawCtx = &mut gfx;
ctx.clear(Color::rgba(1.0, 0.0, 0.0, 1.0));
ctx.save();
ctx.set_transform(TransAffine::new_scaling(2.0, 2.0));
ctx.begin_path();
ctx.circle(16.0, 16.0, 8.0);
ctx.clip_path();
ctx.set_fill_color(Color::rgba(0.0, 0.0, 1.0, 1.0));
ctx.begin_path();
ctx.rect(0.0, 0.0, 64.0, 64.0);
ctx.fill();
ctx.restore();
}
assert!(is_blue(sample(&fb, 32, 32)), "physical centre must be blue");
assert!(
is_blue(sample(&fb, 32, 44)),
"physical radius 12 must be inside; got {:?}",
sample(&fb, 32, 44)
);
assert!(
is_red(sample(&fb, 32, 52)),
"physical radius 20 must be outside; got {:?}",
sample(&fb, 32, 52)
);
assert!(is_red(sample(&fb, 1, 1)), "corner must stay red");
}
#[test]
fn clip_path_edge_is_anti_aliased() {
let fb = clipped_circle_fb();
let blended = (44..54)
.map(|x| sample(&fb, x, 32))
.find(|p| p[0] > 0 && p[0] < 255 && p[2] > 0 && p[2] < 255);
assert!(
blended.is_some(),
"no anti-aliased pixel on the clip edge: {:?}",
(44..54).map(|x| sample(&fb, x, 32)).collect::<Vec<_>>()
);
}