blob: 35882e327ea57a9b5bcc04c82536f4daf485fb71 [file]
// Copyright 2025 the Vello Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Support for blending. See <https://www.w3.org/TR/compositing-1/#introduction> for
//! an introduction as well as the corresponding formulas.
use crate::fine::COLOR_COMPONENTS;
use crate::util::scalar::div_255;
use vello_common::peniko::{BlendMode, Compose, Mix};
pub(crate) mod fill {
use crate::fine::blend::{BlendModeExt, mix};
use crate::fine::{COLOR_COMPONENTS, TILE_HEIGHT_COMPONENTS};
use vello_common::peniko::BlendMode;
pub(crate) fn blend<T: Iterator<Item = [u8; COLOR_COMPONENTS]>>(
target: &mut [u8],
mut color_iter: T,
blend_mode: BlendMode,
) {
for strip in target.chunks_exact_mut(TILE_HEIGHT_COMPONENTS) {
for bg_c in strip.chunks_exact_mut(COLOR_COMPONENTS) {
let mixed_src_color = mix(color_iter.next().unwrap(), bg_c, blend_mode);
blend_mode.compose(&mixed_src_color, bg_c, 255);
}
}
}
}
pub(crate) mod strip {
use crate::fine::blend::{BlendModeExt, mix};
use crate::fine::{COLOR_COMPONENTS, TILE_HEIGHT_COMPONENTS};
use vello_common::peniko::BlendMode;
use vello_common::tile::Tile;
pub(crate) fn blend<
T: Iterator<Item = [u8; COLOR_COMPONENTS]>,
A: Iterator<Item = [u8; Tile::HEIGHT as usize]>,
>(
target: &mut [u8],
mut color_iter: T,
blend_mode: BlendMode,
mut alphas: A,
) {
for bg_col in target.chunks_exact_mut(TILE_HEIGHT_COMPONENTS) {
let masks = alphas.next().unwrap();
for (bg_pix, mask) in bg_col.chunks_exact_mut(Tile::HEIGHT as usize).zip(masks) {
let mixed_src_color = mix(color_iter.next().unwrap(), bg_pix, blend_mode);
blend_mode.compose(&mixed_src_color, bg_pix, mask);
}
}
}
}
fn mix(mut src_c: [u8; 4], bg_c: &[u8], blend_mode: BlendMode) -> [u8; 4] {
// See https://www.w3.org/TR/compositing-1/#blending
let mut mix_bg = [bg_c[0], bg_c[1], bg_c[2], bg_c[3]];
let bg_alpha = mix_bg[3];
// For blending, we need to first unpremultiply everything.
unpremultiply(&mut mix_bg);
unpremultiply(&mut src_c);
let mut mixed = src_c;
// Mix the source and background color. This will then be our
// new source color.
blend_mode.mix(&mut mixed[0..3], &mix_bg[0..3]);
// Account for alpha.
for i in 0..3 {
src_c[i] =
div_255((255 - bg_alpha) as u16 * src_c[i] as u16 + bg_alpha as u16 * mixed[i] as u16)
as u8;
}
// Premultiply again.
premultiply(&mut src_c);
src_c
}
pub(crate) trait BlendModeExt {
fn mix(&self, src: &mut [u8], bg: &[u8]);
fn compose(&self, src_c: &[u8; 4], bg_c: &mut [u8], mask: u8);
}
impl BlendModeExt for BlendMode {
fn mix(&self, src: &mut [u8], bg: &[u8]) {
match self.mix {
Mix::Normal => {}
Mix::Multiply => Multiply::mix(src, bg),
Mix::Screen => Screen::mix(src, bg),
Mix::Overlay => Overlay::mix(src, bg),
Mix::Darken => Darken::mix(src, bg),
Mix::Lighten => Lighten::mix(src, bg),
Mix::ColorDodge => ColorDodge::mix(src, bg),
Mix::ColorBurn => ColorBurn::mix(src, bg),
Mix::HardLight => HardLight::mix(src, bg),
Mix::SoftLight => SoftLight::mix(src, bg),
Mix::Difference => Difference::mix(src, bg),
Mix::Exclusion => Exclusion::mix(src, bg),
Mix::Hue => Hue::mix(src, bg),
Mix::Saturation => Saturation::mix(src, bg),
Mix::Color => Color::mix(src, bg),
Mix::Luminosity => Luminosity::mix(src, bg),
// Same as `Normal`.
Mix::Clip => {}
}
}
fn compose(&self, src_c: &[u8; 4], bg_c: &mut [u8], alpha_mask: u8) {
match self.compose {
Compose::SrcOver => SrcOver::compose(src_c, bg_c, alpha_mask),
Compose::Clear => Clear::compose(src_c, bg_c, alpha_mask),
Compose::Copy => Copy::compose(src_c, bg_c, alpha_mask),
Compose::DestOver => DestOver::compose(src_c, bg_c, alpha_mask),
Compose::Dest => Dest::compose(src_c, bg_c, alpha_mask),
Compose::SrcIn => SrcIn::compose(src_c, bg_c, alpha_mask),
Compose::DestIn => DestIn::compose(src_c, bg_c, alpha_mask),
Compose::SrcOut => SrcOut::compose(src_c, bg_c, alpha_mask),
Compose::DestOut => DestOut::compose(src_c, bg_c, alpha_mask),
Compose::SrcAtop => SrcAtop::compose(src_c, bg_c, alpha_mask),
Compose::DestAtop => DestAtop::compose(src_c, bg_c, alpha_mask),
Compose::Xor => Xor::compose(src_c, bg_c, alpha_mask),
Compose::Plus => Plus::compose(src_c, bg_c, alpha_mask),
// Have not been able to find a formula for this, so just fallback to Plus.
Compose::PlusLighter => SrcOver::compose(src_c, bg_c, alpha_mask),
}
}
}
macro_rules! separable_mix {
($name:ident, $calc:expr) => {
pub(crate) struct $name;
impl $name {
fn mix(source: &mut [u8], background: &[u8]) {
for i in 0..(COLOR_COMPONENTS - 1) {
source[i] = $calc(source[i], background[i]);
}
}
}
};
}
macro_rules! non_separable_mix {
($name:ident, $calc:expr) => {
pub(crate) struct $name;
impl $name {
fn mix(source: &mut [u8], background: &[u8]) {
let cs = to_f32(source);
let cb = to_f32(background);
let res = $calc(cs, cb);
source[..3].copy_from_slice(&from_f32(&res));
}
}
};
}
impl Multiply {
fn single(src: u8, bg: u8) -> u8 {
div_255(src as u16 * bg as u16) as u8
}
}
impl Screen {
fn single(src: u8, bg: u8) -> u8 {
(bg as u16 + src as u16 - div_255(src as u16 * bg as u16)) as u8
}
}
impl HardLight {
fn single(src: u8, bg: u8) -> u8 {
if src <= 127 {
Multiply::single(bg, 2 * src)
} else {
Screen::single(bg, ((2 * src as u16) - 255) as u8)
}
}
}
separable_mix!(Multiply, |cs, cb| div_255(cs as u16 * cb as u16) as u8);
separable_mix!(
Screen,
|cs, cb| (cb as u16 + cs as u16 - div_255(cs as u16 * cb as u16)) as u8
);
separable_mix!(Overlay, |cs, cb| HardLight::single(cb, cs));
separable_mix!(Darken, |cs: u8, cb| cs.min(cb));
separable_mix!(Lighten, |cs: u8, cb| cs.max(cb));
separable_mix!(ColorDodge, |cs: u8, cb| {
if cb == 0 {
0
} else if cs == 255 {
255
} else {
255.min((cb as u16 * 255) / (255 - cs) as u16) as u8
}
});
separable_mix!(ColorBurn, |cs: u8, cb| {
if cb == 255 {
255
} else if cs == 0 {
0
} else {
255 - 255.min((255 - cb) as u16 * 255 / cs as u16) as u8
}
});
separable_mix!(HardLight, |cs: u8, cb| {
if cs <= 127 {
Multiply::single(cb, 2 * cs)
} else {
Screen::single(cb, ((2 * cs as u16) - 255) as u8)
}
});
separable_mix!(SoftLight, |cs: u8, cb| {
let new_src = cs as f32 / 255.0;
let cb = cb as f32 / 255.0;
let d = if cb <= 0.25 {
((16.0 * cb - 12.0) * cb + 4.0) * cb
} else {
cb.sqrt()
};
let res = if new_src <= 0.5 {
cb - (1.0 - 2.0 * new_src) * cb * (1.0 - cb)
} else {
cb + (2.0 * new_src - 1.0) * (d - cb)
};
(res * 255.0 + 0.5) as u8
});
separable_mix!(Difference, |cs: u8, cb| {
if cs <= cb { cb - cs } else { cs - cb }
});
separable_mix!(Exclusion, |cs: u8, cb| ((cs as u16 + cb as u16)
- 2 * div_255(cs as u16 * cb as u16))
as u8);
non_separable_mix!(Hue, |cs, cb| set_lum(&set_sat(&cs, sat(&cb)), lum(&cb)));
non_separable_mix!(Saturation, |cs, cb| set_lum(
&set_sat(&cb, sat(&cs)),
lum(&cb)
));
non_separable_mix!(Color, |cs, cb| set_lum(&cs, lum(&cb)));
non_separable_mix!(Luminosity, |cs, cb| set_lum(&cb, lum(&cs)));
fn to_f32(c: &[u8]) -> [f32; 3] {
let mut nums = [0.0; 3];
for i in 0..3 {
nums[i] = c[i] as f32 / 255.0;
}
nums
}
fn from_f32(c: &[f32; 3]) -> [u8; 3] {
let mut nums = [0; 3];
for i in 0..3 {
nums[i] = (c[i] * 255.0 + 0.5) as u8;
}
nums
}
fn lum(c: &[f32; 3]) -> f32 {
0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
}
fn sat(c: &[f32; 3]) -> f32 {
c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
}
fn clip_color(color: &[f32; 3]) -> [f32; 3] {
let mut c_new = *color;
let l = lum(&c_new);
let n = c_new[0].min(c_new[1].min(c_new[2]));
let x = c_new[0].max(c_new[1].max(c_new[2]));
for c in &mut c_new {
if n < 0.0 {
*c = l + (((*c - l) * l) / (l - n));
}
if x > 1.0 {
*c = l + (((*c - l) * (1.0 - l)) / (x - l));
}
}
c_new
}
fn set_lum(c: &[f32; 3], l: f32) -> [f32; 3] {
let mut c = *c;
let d = l - lum(&c);
c[0] += d;
c[1] += d;
c[2] += d;
clip_color(&c)
}
fn set_sat(c: &[f32; 3], s: f32) -> [f32; 3] {
let mut c = *c;
let (min, tail) = c.split_at_mut(1);
let (mid, max) = tail.split_at_mut(1);
let mut min = &mut min[0];
let mut mid = &mut mid[0];
let mut max = &mut max[0];
if *min > *mid {
core::mem::swap(&mut min, &mut mid);
}
if *min > *max {
core::mem::swap(&mut min, &mut max);
}
if *mid > *max {
core::mem::swap(&mut mid, &mut max);
}
if *max > *min {
*mid = ((*mid - *min) * s) / (*max - *min);
*max = s;
} else {
*mid = 0.0;
*max = 0.0;
}
*min = 0.0;
c
}
fn unpremultiply(color: &mut [u8; 4]) {
let alpha = color[3] as u16;
if alpha != 0 {
for c in &mut color[0..3] {
*c = ((*c as u16 * 255) / alpha) as u8;
}
}
}
fn premultiply(color: &mut [u8; 4]) {
let alpha = color[3] as u16;
for c in &mut color[0..3] {
*c = div_255(*c as u16 * alpha) as u8;
}
}
macro_rules! compose {
($name:ident, $fa:expr, $fb:expr, $sat:expr) => {
struct $name;
impl $name {
fn compose(src_c: &[u8; 4], bg_c: &mut [u8], mask: u8) {
let al_b = bg_c[3] as u16;
let al_s = div_255(src_c[3] as u16 * mask as u16);
for i in 0..4 {
let fa = $fa(al_s, al_b);
let fb = $fb(al_s, al_b);
let src_c = div_255(src_c[i] as u16 * mask as u16);
if $sat {
bg_c[i] = (div_255(src_c * fa) as u8)
.saturating_add(div_255(fb * bg_c[i] as u16) as u8);
} else {
bg_c[i] = (div_255(src_c * fa) + div_255(fb * bg_c[i] as u16)) as u8;
}
}
}
}
};
}
compose!(Clear, |_, _| 0, |_, _| 0, false);
compose!(Copy, |_, _| 255, |_, _| 0, false);
compose!(SrcOver, |_, _| 255, |al_s, _| 255 - al_s, false);
compose!(DestOver, |_, al_b| 255 - al_b, |_, _| 255, false);
compose!(Dest, |_, _| 0, |_, _| 255, false);
compose!(Xor, |_, al_b| 255 - al_b, |al_s, _| 255 - al_s, false);
compose!(SrcIn, |_, al_b| al_b, |_, _| 0, false);
compose!(DestIn, |_, _| 0, |al_s, _| al_s, false);
compose!(SrcOut, |_, al_b| 255 - al_b, |_, _| 0, false);
compose!(DestOut, |_, _| 0, |al_s, _| 255 - al_s, false);
compose!(SrcAtop, |_, al_b| al_b, |al_s, _| 255 - al_s, false);
compose!(DestAtop, |_, al_b| 255 - al_b, |al_s, _| al_s, false);
compose!(Plus, |_, _| 255, |_, _| 255, true);