#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BlendMode {
Normal,
Multiply,
Screen,
Overlay,
Add,
SoftLight,
HardLight,
Difference,
Exclusion,
ColorDodge,
ColorBurn,
Darken,
Lighten,
Subtract,
Divide,
}
impl BlendMode {
pub fn parse(s: &str) -> Self {
match s.to_lowercase().replace(['-', '_', ' '], "").as_str() {
"normal" => Self::Normal,
"multiply" => Self::Multiply,
"screen" => Self::Screen,
"overlay" => Self::Overlay,
"add" | "lineardodge" => Self::Add,
"softlight" => Self::SoftLight,
"hardlight" => Self::HardLight,
"difference" => Self::Difference,
"exclusion" => Self::Exclusion,
"colordodge" => Self::ColorDodge,
"colorburn" => Self::ColorBurn,
"darken" => Self::Darken,
"lighten" => Self::Lighten,
"subtract" | "linearburn" => Self::Subtract,
"divide" => Self::Divide,
_ => Self::Normal,
}
}
}
#[inline]
pub fn alpha_over(bg: &mut [u8; 4], fg: &[u8; 4]) {
let fg_a = fg[3] as f32 / 255.0;
let bg_a = bg[3] as f32 / 255.0;
let out_a = fg_a + bg_a * (1.0 - fg_a);
if out_a < 1e-6 {
*bg = [0, 0, 0, 0];
return;
}
for i in 0..3 {
let fg_c = fg[i] as f32 / 255.0;
let bg_c = bg[i] as f32 / 255.0;
let out_c = (fg_c * fg_a + bg_c * bg_a * (1.0 - fg_a)) / out_a;
bg[i] = (out_c * 255.0).round().clamp(0.0, 255.0) as u8;
}
bg[3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
}
pub fn blend_rows(base: &mut [u8], overlay: &[u8], mode: BlendMode, opacity: f32) {
debug_assert_eq!(base.len(), overlay.len());
debug_assert_eq!(base.len() % 4, 0);
for (bg_px, fg_px) in base.chunks_exact_mut(4).zip(overlay.chunks_exact(4)) {
let fg_a = (fg_px[3] as f32 / 255.0) * opacity;
let blended: [u8; 3] = match mode {
BlendMode::Normal => [fg_px[0], fg_px[1], fg_px[2]],
BlendMode::Multiply => {
let mut out = [0u8; 3];
for i in 0..3 {
let b = bg_px[i] as f32 / 255.0;
let f = fg_px[i] as f32 / 255.0;
out[i] = ((b * f) * 255.0).round().clamp(0.0, 255.0) as u8;
}
out
}
BlendMode::Screen => {
let mut out = [0u8; 3];
for i in 0..3 {
let b = bg_px[i] as f32 / 255.0;
let f = fg_px[i] as f32 / 255.0;
out[i] = ((1.0 - (1.0 - b) * (1.0 - f)) * 255.0)
.round()
.clamp(0.0, 255.0) as u8;
}
out
}
BlendMode::Overlay => {
let mut out = [0u8; 3];
for i in 0..3 {
let b = bg_px[i] as f32 / 255.0;
let f = fg_px[i] as f32 / 255.0;
let v = if b < 0.5 {
2.0 * b * f
} else {
1.0 - 2.0 * (1.0 - b) * (1.0 - f)
};
out[i] = (v * 255.0).round().clamp(0.0, 255.0) as u8;
}
out
}
BlendMode::Add => {
let mut out = [0u8; 3];
for i in 0..3 {
out[i] = (bg_px[i] as u16 + fg_px[i] as u16).min(255) as u8;
}
out
}
BlendMode::SoftLight => {
let mut out = [0u8; 3];
for i in 0..3 {
let b = bg_px[i] as f32 / 255.0;
let f = fg_px[i] as f32 / 255.0;
let v = if f <= 0.5 {
b - (1.0 - 2.0 * f) * b * (1.0 - b)
} else {
let d = if b <= 0.25 {
((16.0 * b - 12.0) * b + 4.0) * b
} else {
b.sqrt()
};
b + (2.0 * f - 1.0) * (d - b)
};
out[i] = (v * 255.0).round().clamp(0.0, 255.0) as u8;
}
out
}
BlendMode::HardLight => {
let mut out = [0u8; 3];
for i in 0..3 {
let b = bg_px[i] as f32 / 255.0;
let f = fg_px[i] as f32 / 255.0;
let v = if f < 0.5 {
2.0 * b * f
} else {
1.0 - 2.0 * (1.0 - b) * (1.0 - f)
};
out[i] = (v * 255.0).round().clamp(0.0, 255.0) as u8;
}
out
}
BlendMode::Difference => {
let mut out = [0u8; 3];
for i in 0..3 {
out[i] = (bg_px[i] as i16 - fg_px[i] as i16).unsigned_abs() as u8;
}
out
}
BlendMode::Exclusion => {
let mut out = [0u8; 3];
for i in 0..3 {
let b = bg_px[i] as f32 / 255.0;
let f = fg_px[i] as f32 / 255.0;
let v = b + f - 2.0 * b * f;
out[i] = (v * 255.0).round().clamp(0.0, 255.0) as u8;
}
out
}
BlendMode::ColorDodge => {
let mut out = [0u8; 3];
for i in 0..3 {
let b = bg_px[i] as f32 / 255.0;
let f = fg_px[i] as f32 / 255.0;
let v = if f >= 1.0 {
1.0
} else {
(b / (1.0 - f)).min(1.0)
};
out[i] = (v * 255.0).round().clamp(0.0, 255.0) as u8;
}
out
}
BlendMode::ColorBurn => {
let mut out = [0u8; 3];
for i in 0..3 {
let b = bg_px[i] as f32 / 255.0;
let f = fg_px[i] as f32 / 255.0;
let v = if f <= 0.0 {
0.0
} else {
1.0 - ((1.0 - b) / f).min(1.0)
};
out[i] = (v * 255.0).round().clamp(0.0, 255.0) as u8;
}
out
}
BlendMode::Darken => {
let mut out = [0u8; 3];
for i in 0..3 {
out[i] = bg_px[i].min(fg_px[i]);
}
out
}
BlendMode::Lighten => {
let mut out = [0u8; 3];
for i in 0..3 {
out[i] = bg_px[i].max(fg_px[i]);
}
out
}
BlendMode::Subtract => {
let mut out = [0u8; 3];
for i in 0..3 {
out[i] = bg_px[i].saturating_sub(fg_px[i]);
}
out
}
BlendMode::Divide => {
let mut out = [0u8; 3];
for i in 0..3 {
let b = bg_px[i] as f32 / 255.0;
let f = fg_px[i] as f32 / 255.0;
let v = if f <= 0.0 { 1.0 } else { (b / f).min(1.0) };
out[i] = (v * 255.0).round().clamp(0.0, 255.0) as u8;
}
out
}
};
let bg_a = bg_px[3] as f32 / 255.0;
let out_a = fg_a + bg_a * (1.0 - fg_a);
if out_a < 1e-6 {
bg_px[0] = 0;
bg_px[1] = 0;
bg_px[2] = 0;
bg_px[3] = 0;
} else {
for i in 0..3 {
let bg_c = bg_px[i] as f32;
let fg_c = blended[i] as f32;
bg_px[i] = (bg_c * (1.0 - fg_a) + fg_c * fg_a)
.round()
.clamp(0.0, 255.0) as u8;
}
bg_px[3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
}
}
}
pub fn composite_at(
bg: &mut [u8],
bg_width: usize,
fg: &[u8],
fg_width: usize,
fg_height: usize,
x: usize,
y: usize,
) {
let bg_stride = bg_width * 4;
let fg_stride = fg_width * 4;
let bg_height = bg.len() / bg_stride;
for row in 0..fg_height {
let bg_y = y + row;
if bg_y >= bg_height {
break;
}
let fg_row_start = row * fg_stride;
let fg_row_end = fg_row_start + fg_stride;
if fg_row_end > fg.len() {
break;
}
let bg_row_start = bg_y * bg_stride;
for col in 0..fg_width {
let bg_x = x + col;
if bg_x >= bg_width {
break;
}
let fg_offset = fg_row_start + col * 4;
let bg_offset = bg_row_start + bg_x * 4;
let mut bg_px = [
bg[bg_offset],
bg[bg_offset + 1],
bg[bg_offset + 2],
bg[bg_offset + 3],
];
let fg_px = [
fg[fg_offset],
fg[fg_offset + 1],
fg[fg_offset + 2],
fg[fg_offset + 3],
];
alpha_over(&mut bg_px, &fg_px);
bg[bg_offset..bg_offset + 4].copy_from_slice(&bg_px);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_alpha_over_opaque_fg() {
let mut bg = [100, 100, 100, 255];
let fg = [200, 50, 50, 255];
alpha_over(&mut bg, &fg);
assert_eq!(bg, [200, 50, 50, 255]); }
#[test]
fn test_alpha_over_transparent_fg() {
let mut bg = [100, 100, 100, 255];
let fg = [200, 50, 50, 0];
alpha_over(&mut bg, &fg);
assert_eq!(bg, [100, 100, 100, 255]); }
#[test]
fn test_alpha_over_half_transparent() {
let mut bg = [0, 0, 0, 255];
let fg = [255, 255, 255, 128]; alpha_over(&mut bg, &fg);
assert!((bg[0] as i32 - 128).abs() <= 2);
}
#[test]
fn test_blend_rows_normal() {
let mut base = [100, 100, 100, 255, 50, 50, 50, 255];
let overlay = [200, 200, 200, 128, 0, 0, 0, 128];
blend_rows(&mut base, &overlay, BlendMode::Normal, 1.0);
assert!((base[0] as i32 - 150).abs() <= 2);
}
#[test]
fn test_blend_rows_multiply() {
let mut base = [200, 200, 200, 255];
let overlay = [128, 128, 128, 255]; blend_rows(&mut base, &overlay, BlendMode::Multiply, 1.0);
assert!((base[0] as i32 - 100).abs() <= 2, "got {}", base[0]);
}
#[test]
fn test_blend_opacity_zero() {
let mut base = [100, 100, 100, 255];
let overlay = [200, 200, 200, 255];
blend_rows(&mut base, &overlay, BlendMode::Normal, 0.0);
assert_eq!(base[0], 100); }
#[test]
fn test_composite_at_clip() {
let mut bg = vec![0u8; 2 * 2 * 4]; let fg = vec![255u8; 2 * 2 * 4]; composite_at(&mut bg, 2, &fg, 2, 2, 1, 1);
let offset = 12;
assert_eq!(bg[offset], 255);
assert_eq!(bg[0], 0);
}
}