blob: 1b36a8e33732408a19b682d6faed0ca2dd9dd0f5 [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
use crate::renderer::Renderer;
use vello_common::color::palette::css::{BLUE, GREEN, LIME, PURPLE, RED, YELLOW};
use vello_common::kurbo::{Affine, Circle, Point, Rect, Shape};
use vello_common::peniko::{BlendMode, Color, Compose, Mix};
use vello_dev_macros::vello_test;
fn compose(ctx: &mut impl Renderer, compose: Compose) {
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcOver));
// Draw the destination layer.
ctx.set_paint(YELLOW.with_alpha(1.0));
ctx.fill_rect(&Rect::new(10.0, 10.0, 70.0, 70.0));
// Draw the source layer.
ctx.push_blend_layer(BlendMode::new(Mix::Normal, compose));
ctx.set_paint(BLUE.with_alpha(1.0));
ctx.fill_rect(&Rect::new(30.0, 30.0, 90.0, 90.0));
// Compose.
ctx.pop_layer();
ctx.pop_layer();
}
#[vello_test]
fn compose_src_over(ctx: &mut impl Renderer) {
compose(ctx, Compose::SrcOver);
}
#[vello_test]
fn compose_xor(ctx: &mut impl Renderer) {
compose(ctx, Compose::Xor);
}
#[vello_test]
fn compose_clear(ctx: &mut impl Renderer) {
compose(ctx, Compose::Clear);
}
#[vello_test(width = 100, height = 100)]
fn compose_clear_empty_layer(ctx: &mut impl Renderer) {
ctx.push_layer(None, None, None, None, None);
ctx.set_paint(BLUE);
ctx.fill_rect(&Rect::new(10.0, 10.0, 90.0, 90.0));
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::Clear));
ctx.pop_layer();
ctx.pop_layer();
}
#[vello_test(width = 100, height = 100)]
fn compose_clear_empty_nested_layer(ctx: &mut impl Renderer) {
ctx.push_layer(None, None, None, None, None);
ctx.set_paint(BLUE);
ctx.fill_rect(&Rect::new(10.0, 10.0, 90.0, 90.0));
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::Clear));
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::Clear));
ctx.pop_layer();
ctx.pop_layer();
ctx.pop_layer();
}
#[vello_test]
fn compose_copy(ctx: &mut impl Renderer) {
compose(ctx, Compose::Copy);
}
#[vello_test]
fn compose_dest(ctx: &mut impl Renderer) {
compose(ctx, Compose::Dest);
}
#[vello_test]
fn compose_dest_over(ctx: &mut impl Renderer) {
compose(ctx, Compose::DestOver);
}
#[vello_test]
fn compose_src_in(ctx: &mut impl Renderer) {
compose(ctx, Compose::SrcIn);
}
#[vello_test]
fn compose_src_out(ctx: &mut impl Renderer) {
compose(ctx, Compose::SrcOut);
}
#[vello_test]
fn compose_dest_in(ctx: &mut impl Renderer) {
compose(ctx, Compose::DestIn);
}
#[vello_test]
fn compose_dest_out(ctx: &mut impl Renderer) {
compose(ctx, Compose::DestOut);
}
#[vello_test]
fn compose_src_atop(ctx: &mut impl Renderer) {
compose(ctx, Compose::SrcAtop);
}
#[vello_test]
fn compose_dest_atop(ctx: &mut impl Renderer) {
compose(ctx, Compose::DestAtop);
}
#[vello_test]
fn compose_plus(ctx: &mut impl Renderer) {
compose(ctx, Compose::Plus);
}
#[vello_test(height = 8, width = 100)]
fn composed_layers_nesting(ctx: &mut impl Renderer) {
ctx.push_blend_layer(BlendMode {
mix: Mix::Normal,
compose: Compose::SrcOver,
});
{
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcOver));
{
ctx.set_paint(BLUE);
ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 4.0));
}
ctx.pop_layer();
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::DestOut));
{
ctx.set_paint(RED);
ctx.fill_rect(&Rect::new(33.0, 0.0, 66.0, 4.0));
}
ctx.pop_layer();
}
ctx.pop_layer();
}
#[vello_test(height = 8, width = 100)]
fn repeatedly_compose_to_bottom_layer(ctx: &mut impl Renderer) {
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcOver));
{
ctx.set_paint(BLUE);
ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 4.0));
}
ctx.pop_layer();
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::DestOut));
{
ctx.set_paint(RED);
ctx.fill_rect(&Rect::new(33.0, 0.0, 66.0, 4.0));
}
ctx.pop_layer();
}
#[vello_test(width = 100, height = 100, transparent)]
fn complex_composed_layers(ctx: &mut impl Renderer) {
ctx.push_blend_layer(BlendMode {
mix: Mix::Normal,
compose: Compose::SrcOver,
});
ctx.set_paint(Color::from_rgb8(240, 240, 240));
ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
// Base layer circles to shine through
ctx.set_paint(YELLOW);
ctx.fill_path(&Circle::new(Point::new(30.0, 30.0), 25.0).to_path(0.1));
ctx.set_paint(BLUE);
ctx.fill_path(&Circle::new(Point::new(70.0, 30.0), 25.0).to_path(0.1));
ctx.set_paint(RED);
ctx.fill_path(&Circle::new(Point::new(30.0, 70.0), 25.0).to_path(0.1));
ctx.set_paint(GREEN);
ctx.fill_path(&Circle::new(Point::new(70.0, 70.0), 25.0).to_path(0.1));
// Layer 1: Simulate clip to center area using compositing
ctx.push_layer(None, None, None, None, None);
{
// Draw the "clip" shape first
ctx.set_paint(Color::WHITE);
ctx.fill_path(&Circle::new(Point::new(50.0, 50.0), 40.0).to_path(0.1));
// Now use SrcIn to clip content to that shape
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcIn));
{
// Layer 2: Semi-transparent purple overlay
ctx.push_layer(None, None, Some(0.7), None, None);
ctx.set_paint(PURPLE);
ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
ctx.pop_layer();
// Layer 3: XOR blend mode with smaller clip simulation
ctx.push_layer(None, None, None, None, None);
{
// Draw the "clip" rectangle
ctx.set_paint(Color::WHITE);
ctx.fill_rect(&Rect::new(25.0, 25.0, 75.0, 75.0));
// Use SrcIn to clip the XOR content
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcIn));
{
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::Xor));
// Draw overlapping circles with XOR
ctx.set_paint(RED);
ctx.fill_path(&Circle::new(Point::new(40.0, 40.0), 20.0).to_path(0.1));
ctx.set_paint(BLUE);
ctx.fill_path(&Circle::new(Point::new(60.0, 40.0), 20.0).to_path(0.1));
ctx.pop_layer();
}
ctx.pop_layer();
}
ctx.pop_layer();
// Layer 4: Nested opacity with Plus blend
ctx.push_layer(None, None, Some(0.5), None, None);
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::Plus));
{
ctx.set_paint(YELLOW);
ctx.fill_path(&Circle::new(Point::new(50.0, 60.0), 15.0).to_path(0.1));
// Layer 5: "Clip" simulation with SrcIn
ctx.push_layer(None, None, None, None, None);
{
ctx.set_paint(Color::WHITE);
ctx.fill_path(&Circle::new(Point::new(50.0, 50.0), 30.0).to_path(0.1));
// Use SrcIn twice (once for the clip, once for the original SrcIn)
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcIn));
{
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcIn));
{
ctx.set_paint(GREEN);
ctx.fill_rect(&Rect::new(35.0, 35.0, 65.0, 65.0));
}
ctx.pop_layer();
}
ctx.pop_layer();
}
ctx.pop_layer();
}
ctx.pop_layer();
ctx.pop_layer();
// Layer 6: Final overlay with DestOut to create a hole
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::DestOut));
ctx.set_paint(Color::BLACK);
ctx.fill_path(&Circle::new(Point::new(50.0, 50.0), 10.0).to_path(0.1));
ctx.pop_layer();
}
ctx.pop_layer();
}
ctx.pop_layer();
ctx.pop_layer();
}
#[vello_test(
width = 100,
height = 100,
transparent,
cpu_u8_tolerance = 2,
hybrid_tolerance = 2
)]
fn deep_compose(ctx: &mut impl Renderer) {
const INITIAL_RADIUS: f64 = 48.0;
const RADIUS_DECREMENT: f64 = 4.5;
const LAYER_COUNT: usize = 10;
const CENTER: Point = Point::new(50.0, 50.0);
const COLORS: [Color; LAYER_COUNT] = [
Color::from_rgba8(120, 0, 0, 50),
Color::from_rgba8(120, 60, 0, 50),
Color::from_rgba8(120, 120, 0, 50),
Color::from_rgba8(60, 120, 0, 50),
Color::from_rgba8(0, 120, 0, 50),
Color::from_rgba8(0, 120, 60, 50),
Color::from_rgba8(0, 120, 120, 50),
Color::from_rgba8(0, 60, 120, 50),
Color::from_rgba8(0, 0, 120, 50),
Color::from_rgba8(60, 0, 120, 50),
];
// Composition modes are intentionally "additive".
const COMPOSE_MODES: [Compose; LAYER_COUNT] = [
Compose::SrcOver,
Compose::Plus,
Compose::SrcOver,
Compose::DestOver,
Compose::SrcOver,
Compose::Plus,
Compose::SrcAtop,
Compose::Plus,
Compose::SrcOver,
Compose::SrcOver,
];
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcOver));
ctx.set_paint(Color::BLACK);
ctx.fill_rect(&Rect::new(0.0, 0.0, 100.0, 100.0));
let mut radius = INITIAL_RADIUS;
for i in 0..LAYER_COUNT {
ctx.push_blend_layer(BlendMode::new(Mix::Normal, COMPOSE_MODES[i]));
ctx.set_paint(COLORS[i]);
ctx.fill_path(&Circle::new(CENTER, radius).to_path(0.1));
radius -= RADIUS_DECREMENT;
}
for _ in 0..=LAYER_COUNT {
ctx.pop_layer();
}
}
// Ensure that compose and mix work together in the same blend layer.
#[vello_test(width = 160, height = 160)]
fn mix_compose_combined_test_matrix(ctx: &mut impl Renderer) {
let mix_modes = [
Mix::Normal,
Mix::Multiply,
Mix::Screen,
Mix::Overlay,
Mix::Darken,
Mix::Lighten,
Mix::ColorDodge,
Mix::ColorBurn,
Mix::HardLight,
Mix::SoftLight,
Mix::Difference,
Mix::Exclusion,
Mix::Hue,
Mix::Saturation,
Mix::Color,
Mix::Luminosity,
];
let compose_modes = [
Compose::Clear,
Compose::Copy,
Compose::Dest,
Compose::SrcOver,
Compose::DestOver,
Compose::SrcIn,
Compose::DestIn,
Compose::SrcOut,
Compose::DestOut,
Compose::SrcAtop,
Compose::DestAtop,
Compose::Xor,
Compose::Plus,
Compose::PlusLighter,
];
let cell_size = 10.0;
ctx.set_paint(Color::from_rgb8(30, 30, 30));
ctx.fill_rect(&Rect::new(0.0, 0.0, 160.0, 160.0));
for (row, mix_mode) in mix_modes.iter().enumerate() {
for (col, compose_mode) in compose_modes.iter().enumerate() {
let x = (col as f64) * cell_size;
let y = (row as f64) * cell_size;
ctx.set_transform(Affine::translate((x, y)));
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcOver));
// Draw magenta rectangle for destination.
ctx.set_paint(Color::from_rgb8(200, 0, 200).with_alpha(0.7));
ctx.fill_rect(&Rect::new(0.0, 0.0, cell_size * 0.7, cell_size * 0.7));
// Push mix + compose blend layer with fill
ctx.push_blend_layer(BlendMode::new(*mix_mode, *compose_mode));
ctx.set_paint(Color::from_rgb8(10, 200, 200).with_alpha(0.7)); // Cyan
ctx.fill_rect(&Rect::new(
cell_size * 0.3,
cell_size * 0.3,
cell_size,
cell_size,
));
ctx.pop_layer();
ctx.pop_layer();
}
}
}
fn compose_non_isolated(ctx: &mut impl Renderer, compose: Compose) {
// Just to isolate from the white background.
ctx.push_blend_layer(BlendMode::new(Mix::Normal, Compose::SrcOver));
let rect1 = Rect::new(10.5, 10.5, 70.5, 70.5);
ctx.set_paint(BLUE.with_alpha(0.5));
ctx.fill_rect(&rect1);
ctx.set_blend_mode(BlendMode::new(Mix::Normal, compose));
let rect2 = Rect::new(30.5, 30.5, 90.5, 90.5);
ctx.set_paint(LIME.with_alpha(0.5));
ctx.fill_rect(&rect2);
ctx.pop_layer();
}
#[vello_test]
fn compose_non_isolated_src_over(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::SrcOver);
}
#[vello_test]
fn compose_non_isolated_xor(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::Xor);
}
#[vello_test(skip_hybrid)]
fn compose_non_isolated_clear(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::Clear);
}
#[vello_test(skip_hybrid)]
fn compose_non_isolated_copy(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::Copy);
}
#[vello_test]
fn compose_non_isolated_dest(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::Dest);
}
#[vello_test]
fn compose_non_isolated_dest_over(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::DestOver);
}
#[vello_test(skip_hybrid)]
fn compose_non_isolated_src_in(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::SrcIn);
}
#[vello_test(skip_hybrid)]
fn compose_non_isolated_src_out(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::SrcOut);
}
#[vello_test(skip_hybrid)]
fn compose_non_isolated_dest_in(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::DestIn);
}
#[vello_test]
fn compose_non_isolated_dest_out(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::DestOut);
}
#[vello_test]
fn compose_non_isolated_src_atop(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::SrcAtop);
}
#[vello_test(skip_hybrid)]
fn compose_non_isolated_dest_atop(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::DestAtop);
}
#[vello_test]
fn compose_non_isolated_plus(ctx: &mut impl Renderer) {
compose_non_isolated(ctx, Compose::Plus);
}