use crate::framebuffer::{pack_rgba, unpack, Framebuffer};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BlendMode {
Normal,
Multiply,
Screen,
Overlay,
Darken,
Lighten,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RgbaUnit {
pub r: f32,
pub g: f32,
pub b: f32,
pub a: f32,
}
impl RgbaUnit {
pub fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Self {
r: r.clamp(0.0, 1.0),
g: g.clamp(0.0, 1.0),
b: b.clamp(0.0, 1.0),
a: a.clamp(0.0, 1.0),
}
}
pub fn from_bytes(r: u8, g: u8, b: u8, a: u8) -> Self {
Self {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: a as f32 / 255.0,
}
}
pub fn to_bytes(self) -> (u8, u8, u8, u8) {
(
(self.r.clamp(0.0, 1.0) * 255.0).round() as u8,
(self.g.clamp(0.0, 1.0) * 255.0).round() as u8,
(self.b.clamp(0.0, 1.0) * 255.0).round() as u8,
(self.a.clamp(0.0, 1.0) * 255.0).round() as u8,
)
}
pub fn premultiply(self) -> Self {
Self {
r: self.r * self.a,
g: self.g * self.a,
b: self.b * self.a,
a: self.a,
}
}
pub fn unpremultiply(self) -> Self {
if self.a <= f32::EPSILON {
Self {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
}
} else {
Self {
r: self.r / self.a,
g: self.g / self.a,
b: self.b / self.a,
a: self.a,
}
}
}
}
pub fn to_unit(px: u32) -> RgbaUnit {
let (r, g, b, a) = unpack(px);
RgbaUnit::from_bytes(r, g, b, a)
}
pub fn from_unit(p: RgbaUnit) -> u32 {
let (r, g, b, a) = p.to_bytes();
pack_rgba(r, g, b, a)
}
#[inline]
fn to_premult(r: u8, g: u8, b: u8, a: u8) -> (u16, u16, u16, u16) {
let a16 = a as u16;
(
(r as u16 * a16 + 127) / 255,
(g as u16 * a16 + 127) / 255,
(b as u16 * a16 + 127) / 255,
a16,
)
}
#[inline]
fn from_premult(pr: u16, pg: u16, pb: u16, a: u16) -> (u8, u8, u8, u8) {
if a == 0 {
return (0, 0, 0, 0);
}
let half = a / 2;
(
((pr * 255 + half) / a).min(255) as u8,
((pg * 255 + half) / a).min(255) as u8,
((pb * 255 + half) / a).min(255) as u8,
a as u8,
)
}
#[inline]
#[allow(clippy::too_many_arguments)]
pub(crate) fn blend_over_premult_u8(
src_r: u8,
src_g: u8,
src_b: u8,
src_a: u8,
dst_r: u8,
dst_g: u8,
dst_b: u8,
dst_a: u8,
) -> (u8, u8, u8, u8) {
let (spr, spg, spb, sa) = to_premult(src_r, src_g, src_b, src_a);
let (dpr, dpg, dpb, da) = to_premult(dst_r, dst_g, dst_b, dst_a);
let inv_sa = 255u16 - sa;
let out_r = spr + (dpr * inv_sa + 127) / 255;
let out_g = spg + (dpg * inv_sa + 127) / 255;
let out_b = spb + (dpb * inv_sa + 127) / 255;
let out_a = sa + (da * inv_sa + 127) / 255;
from_premult(out_r, out_g, out_b, out_a)
}
pub fn blend_mode(mode: BlendMode, src: RgbaUnit, dst: RgbaUnit) -> RgbaUnit {
match mode {
BlendMode::Normal => over(src, dst),
BlendMode::Multiply => mode_combine(src, dst, |a, b| a * b),
BlendMode::Screen => mode_combine(src, dst, |a, b| a + b - a * b),
BlendMode::Overlay => mode_combine(src, dst, |cs, cb| {
if cb <= 0.5 {
2.0 * cs * cb
} else {
1.0 - 2.0 * (1.0 - cs) * (1.0 - cb)
}
}),
BlendMode::Darken => mode_combine(src, dst, f32::min),
BlendMode::Lighten => mode_combine(src, dst, f32::max),
}
}
fn over(src: RgbaUnit, dst: RgbaUnit) -> RgbaUnit {
let out_a = src.a + dst.a * (1.0 - src.a);
if out_a <= f32::EPSILON {
return RgbaUnit {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
}
let blend = |s: f32, d: f32| (s * src.a + d * dst.a * (1.0 - src.a)) / out_a;
RgbaUnit {
r: blend(src.r, dst.r),
g: blend(src.g, dst.g),
b: blend(src.b, dst.b),
a: out_a,
}
}
fn mode_combine<F>(src: RgbaUnit, dst: RgbaUnit, blend_fn: F) -> RgbaUnit
where
F: Fn(f32, f32) -> f32,
{
let out_a = src.a + dst.a * (1.0 - src.a);
if out_a <= f32::EPSILON {
return RgbaUnit {
r: 0.0,
g: 0.0,
b: 0.0,
a: 0.0,
};
}
let channel = |cs: f32, cb: f32| -> f32 {
let b = blend_fn(cs.clamp(0.0, 1.0), cb.clamp(0.0, 1.0));
let v = (1.0 - dst.a) * cs * src.a + (1.0 - src.a) * cb * dst.a + src.a * dst.a * b;
(v / out_a).clamp(0.0, 1.0)
};
RgbaUnit {
r: channel(src.r, dst.r),
g: channel(src.g, dst.g),
b: channel(src.b, dst.b),
a: out_a,
}
}
pub fn blend_pixel(fb: &mut Framebuffer, x: u32, y: u32, src: RgbaUnit, mode: BlendMode) {
if x >= fb.width() || y >= fb.height() {
return;
}
let Some(dst_px) = fb.get(x, y) else { return };
let dst = to_unit(dst_px);
let out = blend_mode(mode, src, dst);
fb.set(x, y, from_unit(out));
}
pub fn composite_into(
fb: &mut Framebuffer,
src: &[u8],
w: u32,
h: u32,
dst_x: i64,
dst_y: i64,
mode: BlendMode,
) -> usize {
if w == 0 || h == 0 || src.len() < (w * h * 4) as usize {
return 0;
}
let mut written = 0;
for j in 0..h {
for i in 0..w {
let si = ((j * w + i) * 4) as usize;
let r = src[si];
let g = src[si + 1];
let b = src[si + 2];
let a = src[si + 3];
if a == 0 {
continue;
}
let px = dst_x + i as i64;
let py = dst_y + j as i64;
if px < 0 || py < 0 {
continue;
}
let pu = px as u32;
let pv = py as u32;
if pu >= fb.width() || pv >= fb.height() {
continue;
}
if mode == BlendMode::Normal {
let dst_px = fb.get(pu, pv).unwrap_or(0);
let (dr, dg, db, da) = unpack(dst_px);
let (or_, og, ob, oa) = blend_over_premult_u8(r, g, b, a, dr, dg, db, da);
fb.set(pu, pv, crate::framebuffer::pack_rgba(or_, og, ob, oa));
} else {
blend_pixel(fb, pu, pv, RgbaUnit::from_bytes(r, g, b, a), mode);
}
written += 1;
}
}
written
}
#[cfg(test)]
mod tests {
use super::*;
use oxiui_core::Color;
fn approx_eq(a: f32, b: f32, eps: f32) -> bool {
(a - b).abs() <= eps
}
#[test]
fn round_trip_unit_bytes() {
let p = RgbaUnit::from_bytes(123, 45, 200, 99);
let (r, g, b, a) = p.to_bytes();
assert_eq!((r, g, b, a), (123, 45, 200, 99));
}
#[test]
fn premultiply_inverse() {
let p = RgbaUnit::new(0.8, 0.4, 0.2, 0.5);
let q = p.premultiply().unpremultiply();
assert!(approx_eq(q.r, p.r, 1e-5));
assert!(approx_eq(q.g, p.g, 1e-5));
assert!(approx_eq(q.b, p.b, 1e-5));
assert!(approx_eq(q.a, p.a, 1e-5));
}
#[test]
fn premultiply_zero_alpha_safe() {
let p = RgbaUnit::new(0.9, 0.9, 0.9, 0.0);
let q = p.premultiply().unpremultiply();
assert!(approx_eq(q.a, 0.0, 1e-5));
assert!(approx_eq(q.r, 0.0, 1e-5));
}
#[test]
fn multiply_white_is_identity() {
let src = RgbaUnit::from_bytes(255, 255, 255, 255);
let dst = RgbaUnit::from_bytes(255, 0, 0, 255);
let out = blend_mode(BlendMode::Multiply, src, dst);
let (r, g, b, a) = out.to_bytes();
assert_eq!((r, g, b, a), (255, 0, 0, 255));
}
#[test]
fn screen_black_is_identity() {
let src = RgbaUnit::from_bytes(0, 0, 0, 255);
let dst = RgbaUnit::from_bytes(80, 120, 200, 255);
let out = blend_mode(BlendMode::Screen, src, dst);
let (r, g, b, _) = out.to_bytes();
assert!((r as i32 - 80).abs() <= 1);
assert!((g as i32 - 120).abs() <= 1);
assert!((b as i32 - 200).abs() <= 1);
}
#[test]
fn darken_picks_min_per_channel() {
let src = RgbaUnit::from_bytes(100, 200, 50, 255);
let dst = RgbaUnit::from_bytes(150, 50, 50, 255);
let out = blend_mode(BlendMode::Darken, src, dst);
let (r, g, b, _) = out.to_bytes();
assert!((r as i32 - 100).abs() <= 1);
assert!((g as i32 - 50).abs() <= 1);
assert!((b as i32 - 50).abs() <= 1);
}
#[test]
fn lighten_picks_max_per_channel() {
let src = RgbaUnit::from_bytes(100, 200, 50, 255);
let dst = RgbaUnit::from_bytes(150, 50, 50, 255);
let out = blend_mode(BlendMode::Lighten, src, dst);
let (r, g, b, _) = out.to_bytes();
assert!((r as i32 - 150).abs() <= 1);
assert!((g as i32 - 200).abs() <= 1);
assert!((b as i32 - 50).abs() <= 1);
}
#[test]
fn overlay_changes_with_dst_mid() {
let src = RgbaUnit::from_bytes(128, 128, 128, 255);
let dst = RgbaUnit::from_bytes(128, 128, 128, 255);
let out = blend_mode(BlendMode::Overlay, src, dst);
let (r, _, _, a) = out.to_bytes();
assert_eq!(a, 255);
let _ = r;
}
#[test]
fn over_zero_alpha_safe() {
let src = RgbaUnit::new(0.0, 0.0, 0.0, 0.0);
let dst = RgbaUnit::new(0.0, 0.0, 0.0, 0.0);
let out = blend_mode(BlendMode::Normal, src, dst);
let (_, _, _, a) = out.to_bytes();
assert_eq!(a, 0);
}
#[test]
fn composite_into_writes_pixels() {
let mut fb = Framebuffer::with_fill(4, 4, Color(0, 0, 0, 255));
let src = vec![
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
];
let written = composite_into(&mut fb, &src, 2, 2, 1, 1, BlendMode::Normal);
assert_eq!(written, 4);
let result = fb.get_rgba(1, 1).unwrap_or_default();
assert_eq!(result, (255, 255, 255, 255));
}
#[test]
fn blend_pixel_out_of_bounds_noop() {
let mut fb = Framebuffer::with_fill(2, 2, Color(10, 20, 30, 255));
blend_pixel(
&mut fb,
99,
99,
RgbaUnit::from_bytes(255, 255, 255, 255),
BlendMode::Normal,
);
let v = fb.get_rgba(0, 0).unwrap_or((0, 0, 0, 0));
assert_eq!(v, (10, 20, 30, 255));
}
#[test]
fn blend_multiply_black_is_black() {
let black = RgbaUnit::from_bytes(0, 0, 0, 255);
let src = RgbaUnit::from_bytes(200, 100, 50, 255);
let out = blend_mode(BlendMode::Multiply, src, black);
let (r, g, b, _) = out.to_bytes();
assert_eq!((r, g, b), (0, 0, 0), "multiply with black must give black");
}
#[test]
fn blend_screen_white_is_white() {
let white = RgbaUnit::from_bytes(255, 255, 255, 255);
let src = RgbaUnit::from_bytes(100, 150, 200, 255);
let out = blend_mode(BlendMode::Screen, src, white);
let (r, g, b, _) = out.to_bytes();
assert_eq!(
(r, g, b),
(255, 255, 255),
"screen with white must give white"
);
}
#[test]
fn premultiply_roundtrip() {
let p = RgbaUnit::new(0.7, 0.3, 0.9, 0.6);
let q = p.premultiply().unpremultiply();
assert!(approx_eq(q.r, p.r, 1e-4), "r: {:.5} vs {:.5}", q.r, p.r);
assert!(approx_eq(q.g, p.g, 1e-4), "g: {:.5} vs {:.5}", q.g, p.g);
assert!(approx_eq(q.b, p.b, 1e-4), "b: {:.5} vs {:.5}", q.b, p.b);
assert!(approx_eq(q.a, p.a, 1e-5), "a: {:.5} vs {:.5}", q.a, p.a);
}
#[test]
fn test_premult_blend_golden() {
let (r, g, b, a) = blend_over_premult_u8(255, 0, 0, 128, 255, 255, 255, 255);
assert!((r as i32 - 255).abs() <= 2, "r={r} expected ~255");
assert!((g as i32 - 127).abs() <= 2, "g={g} expected ~127");
assert!((b as i32 - 127).abs() <= 2, "b={b} expected ~127");
assert!((a as i32 - 255).abs() <= 2, "a={a} expected ~255");
}
#[test]
fn test_blend_signatures_stable() {
let src = RgbaUnit::from_bytes(128, 64, 32, 200);
let dst = RgbaUnit::from_bytes(10, 20, 30, 255);
let _ = blend_mode(BlendMode::Normal, src, dst);
let _ = blend_mode(BlendMode::Multiply, src, dst);
let _ = blend_mode(BlendMode::Screen, src, dst);
let _ = blend_mode(BlendMode::Overlay, src, dst);
let _ = blend_mode(BlendMode::Darken, src, dst);
let _ = blend_mode(BlendMode::Lighten, src, dst);
let px = crate::framebuffer::pack_rgba(100, 150, 200, 180);
let unit = to_unit(px);
let _ = from_unit(unit);
let mut fb = Framebuffer::with_fill(4, 4, Color(0, 0, 0, 255));
blend_pixel(&mut fb, 0, 0, src, BlendMode::Normal);
blend_pixel(&mut fb, 99, 99, src, BlendMode::Screen);
let src_bytes = [255u8, 0, 0, 128];
let written = composite_into(&mut fb, &src_bytes, 1, 1, 0, 0, BlendMode::Normal);
assert!(written <= 1);
}
}