#![allow(dead_code)]
#![allow(clippy::cast_precision_loss)]
#[must_use]
pub fn multiply(a: f32, b: f32) -> f32 {
(a * b).clamp(0.0, 1.0)
}
#[must_use]
pub fn screen(a: f32, b: f32) -> f32 {
let result = 1.0 - (1.0 - a) * (1.0 - b);
result.clamp(0.0, 1.0)
}
#[must_use]
pub fn overlay(base: f32, blend: f32) -> f32 {
let result = if base < 0.5 {
2.0 * base * blend
} else {
1.0 - 2.0 * (1.0 - base) * (1.0 - blend)
};
result.clamp(0.0, 1.0)
}
#[must_use]
pub fn hard_light(base: f32, blend: f32) -> f32 {
overlay(blend, base)
}
#[must_use]
pub fn soft_light(base: f32, blend: f32) -> f32 {
let result = if blend < 0.5 {
base - (1.0 - 2.0 * blend) * base * (1.0 - base)
} else {
let d = if base < 0.25 {
((16.0 * base - 12.0) * base + 4.0) * base
} else {
base.sqrt()
};
base + (2.0 * blend - 1.0) * (d - base)
};
result.clamp(0.0, 1.0)
}
#[must_use]
pub fn difference(a: f32, b: f32) -> f32 {
(a - b).abs().clamp(0.0, 1.0)
}
#[must_use]
pub fn exclusion(a: f32, b: f32) -> f32 {
(a + b - 2.0 * a * b).clamp(0.0, 1.0)
}
#[must_use]
pub fn linear_dodge(a: f32, b: f32) -> f32 {
(a + b).clamp(0.0, 1.0)
}
#[must_use]
pub fn linear_burn(a: f32, b: f32) -> f32 {
(a + b - 1.0).clamp(0.0, 1.0)
}
pub fn blend_buffers(a: &[f32], b: &[f32], output: &mut [f32], mode: BlendMode, mix: f32) {
let mix = mix.clamp(0.0, 1.0);
let len = a.len().min(b.len()).min(output.len());
for i in 0..len {
let blended = mode.apply(a[i], b[i]);
output[i] = a[i] * (1.0 - mix) + blended * mix;
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BlendMode {
Multiply,
Screen,
Overlay,
HardLight,
SoftLight,
Difference,
Exclusion,
LinearDodge,
LinearBurn,
}
impl BlendMode {
#[must_use]
pub fn apply(self, a: f32, b: f32) -> f32 {
match self {
Self::Multiply => multiply(a, b),
Self::Screen => screen(a, b),
Self::Overlay => overlay(a, b),
Self::HardLight => hard_light(a, b),
Self::SoftLight => soft_light(a, b),
Self::Difference => difference(a, b),
Self::Exclusion => exclusion(a, b),
Self::LinearDodge => linear_dodge(a, b),
Self::LinearBurn => linear_burn(a, b),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_multiply_extremes() {
assert_eq!(multiply(0.0, 1.0), 0.0);
assert_eq!(multiply(1.0, 1.0), 1.0);
assert_eq!(multiply(0.0, 0.0), 0.0);
}
#[test]
fn test_multiply_midpoint() {
let result = multiply(0.5, 0.5);
assert!((result - 0.25).abs() < 1e-6);
}
#[test]
fn test_screen_extremes() {
assert_eq!(screen(0.0, 0.0), 0.0);
assert_eq!(screen(1.0, 0.0), 1.0);
assert_eq!(screen(1.0, 1.0), 1.0);
}
#[test]
fn test_screen_midpoint() {
let result = screen(0.5, 0.5);
assert!((result - 0.75).abs() < 1e-6);
}
#[test]
fn test_screen_always_brighter_than_inputs() {
let a = 0.3_f32;
let b = 0.4_f32;
let result = screen(a, b);
assert!(result >= a);
assert!(result >= b);
}
#[test]
fn test_multiply_always_darker_than_inputs() {
let a = 0.6_f32;
let b = 0.7_f32;
let result = multiply(a, b);
assert!(result <= a);
assert!(result <= b);
}
#[test]
fn test_overlay_dark_region() {
let result = overlay(0.25, 0.5);
assert!((result - 0.25).abs() < 1e-6);
}
#[test]
fn test_overlay_bright_region() {
let result = overlay(0.75, 0.5);
assert!((result - 0.75).abs() < 1e-6);
}
#[test]
fn test_hard_light_is_overlay_swapped() {
let a = 0.3_f32;
let b = 0.6_f32;
assert!((hard_light(a, b) - overlay(b, a)).abs() < 1e-6);
}
#[test]
fn test_soft_light_neutral_at_half() {
let base = 0.4_f32;
let result = soft_light(base, 0.5);
assert!((result - base).abs() < 1e-6);
}
#[test]
fn test_difference_symmetry() {
assert!((difference(0.3, 0.7) - difference(0.7, 0.3)).abs() < 1e-6);
}
#[test]
fn test_exclusion_neutral_at_zero() {
assert_eq!(exclusion(0.5, 0.0), 0.5);
}
#[test]
fn test_linear_dodge_clamped() {
assert_eq!(linear_dodge(0.8, 0.8), 1.0);
}
#[test]
fn test_linear_burn_clamped() {
assert_eq!(linear_burn(0.1, 0.1), 0.0);
}
#[test]
fn test_blend_mode_enum_multiply() {
let result = BlendMode::Multiply.apply(0.5, 0.5);
assert!((result - 0.25).abs() < 1e-6);
}
#[test]
fn test_blend_buffers() {
let a = vec![0.0, 0.5, 1.0];
let b = vec![1.0, 0.5, 0.0];
let mut out = vec![0.0; 3];
blend_buffers(&a, &b, &mut out, BlendMode::Screen, 1.0);
assert!((out[0] - 1.0).abs() < 1e-6);
assert!((out[1] - 0.75).abs() < 1e-6);
assert!((out[2] - 1.0).abs() < 1e-6);
}
#[test]
fn test_blend_buffers_mix_zero_passthrough() {
let a = vec![0.3, 0.6, 0.9];
let b = vec![0.1, 0.2, 0.3];
let mut out = vec![0.0; 3];
blend_buffers(&a, &b, &mut out, BlendMode::Multiply, 0.0);
for i in 0..3 {
assert!((out[i] - a[i]).abs() < 1e-6);
}
}
#[test]
fn test_all_modes_clamp() {
let modes = [
BlendMode::Multiply,
BlendMode::Screen,
BlendMode::Overlay,
BlendMode::HardLight,
BlendMode::SoftLight,
BlendMode::Difference,
BlendMode::Exclusion,
BlendMode::LinearDodge,
BlendMode::LinearBurn,
];
for mode in modes {
let result = mode.apply(1.2, -0.2);
assert!(
result >= 0.0 && result <= 1.0,
"Mode {mode:?} out of range: {result}"
);
}
}
}