This commit is contained in:
2026-07-09 02:59:53 +03:00
commit 7ace048d94
817 changed files with 234289 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "hcie-blend"
version = "0.1.0"
edition = "2021"
[dependencies]
hcie-color = { path = "../hcie-color" }
rand = "0.8"
rayon = "1.10"
serde = { version = "1.0", features = ["derive"] }
[lib]
crate-type = ["staticlib", "rlib"]
[dev-dependencies]
rstest = "0.23"
proptest = "1.5"
approx = "0.5"
egui = "0.34"
eframe = { version = "0.34", default-features = false, features = ["default_fonts", "glow"] }
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
+214
View File
@@ -0,0 +1,214 @@
#![allow(deprecated)]
//! Visual test GUI for the decoupled `hcie-blend` library using `egui`.
//! Compiles only when running `cargo run --example blend_tester`.
use eframe::egui;
use hcie_blend::blend_buffers;
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([600.0, 400.0])
.with_title("HCIE Blend Engine Visual Tester"),
..Default::default()
};
eframe::run_native(
"HCIE Blend Visual Tester",
options,
Box::new(|cc| Ok(Box::new(BlendTesterApp::new(cc)))),
)
}
struct BlendTesterApp {
background: Vec<u8>,
foreground: Vec<u8>,
blended: Vec<u8>,
width: u32,
height: u32,
opacity: f32,
blend_mode: i32,
texture: Option<egui::TextureHandle>,
}
impl BlendTesterApp {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
let width = 256;
let height = 256;
let len = (width * height * 4) as usize;
// Generate background: Checkerboard (White / Light Gray)
let mut background = vec![0u8; len];
for y in 0..height {
for x in 0..width {
let i = ((y * width + x) * 4) as usize;
let is_gray = ((x / 16) + (y / 16)) % 2 == 0;
let c = if is_gray { 200 } else { 255 };
background[i] = c;
background[i + 1] = c;
background[i + 2] = c;
background[i + 3] = 255;
}
}
// Generate foreground: A soft-edged red circle in the center
let mut foreground = vec![0u8; len];
let cx = width as f32 / 2.0;
let cy = height as f32 / 2.0;
let r = 80.0;
for y in 0..height {
for x in 0..width {
let i = ((y * width + x) * 4) as usize;
let dx = x as f32 - cx;
let dy = y as f32 - cy;
let dist = (dx * dx + dy * dy).sqrt();
// Soft edge alpha transition
let alpha = if dist < r - 10.0 {
255
} else if dist < r + 10.0 {
(((r + 10.0 - dist) / 20.0) * 255.0) as u8
} else {
0
};
foreground[i] = 255; // Red
foreground[i + 1] = 50; // Orange-ish tint
foreground[i + 2] = 50;
foreground[i + 3] = alpha;
}
}
let blended = background.clone();
let mut app = Self {
background,
foreground,
blended,
width,
height,
opacity: 1.0,
blend_mode: 0, // Normal
texture: None,
};
app.update_blended();
app
}
fn update_blended(&mut self) {
// Reset blended buffer to background first
self.blended.copy_from_slice(&self.background);
// Run the blending function
let mode = match self.blend_mode {
0 => hcie_blend::BlendMode::Normal,
1 => hcie_blend::BlendMode::Dissolve,
2 => hcie_blend::BlendMode::Darken,
3 => hcie_blend::BlendMode::Multiply,
4 => hcie_blend::BlendMode::ColorBurn,
5 => hcie_blend::BlendMode::LinearBurn,
6 => hcie_blend::BlendMode::DarkerColor,
7 => hcie_blend::BlendMode::Lighten,
8 => hcie_blend::BlendMode::Screen,
9 => hcie_blend::BlendMode::ColorDodge,
10 => hcie_blend::BlendMode::LinearDodge,
11 => hcie_blend::BlendMode::LighterColor,
12 => hcie_blend::BlendMode::Overlay,
13 => hcie_blend::BlendMode::SoftLight,
14 => hcie_blend::BlendMode::HardLight,
15 => hcie_blend::BlendMode::VividLight,
16 => hcie_blend::BlendMode::LinearLight,
17 => hcie_blend::BlendMode::PinLight,
18 => hcie_blend::BlendMode::HardMix,
19 => hcie_blend::BlendMode::Difference,
20 => hcie_blend::BlendMode::Exclusion,
21 => hcie_blend::BlendMode::Subtract,
22 => hcie_blend::BlendMode::Divide,
23 => hcie_blend::BlendMode::Hue,
24 => hcie_blend::BlendMode::Saturation,
25 => hcie_blend::BlendMode::Color,
26 => hcie_blend::BlendMode::Luminosity,
_ => hcie_blend::BlendMode::Normal,
};
blend_buffers(
&mut self.blended,
&self.foreground,
mode,
self.opacity,
);
self.texture = None; // Invalidate texture to force reload
}
}
impl eframe::App for BlendTesterApp {
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
let ctx = ui.ctx().clone();
let modes = [
(0, "Normal"), (1, "Dissolve"), (2, "Darken"), (3, "Multiply"),
(4, "Color Burn"), (5, "Linear Burn"), (6, "Darker Color"),
(7, "Lighten"), (8, "Screen"), (9, "Color Dodge"), (10, "Linear Dodge"),
(11, "Lighter Color"), (12, "Overlay"), (13, "Soft Light"), (14, "Hard Light"),
(15, "Vivid Light"), (16, "Linear Light"), (17, "Pin Light"), (18, "Hard Mix"),
(19, "Difference"), (20, "Exclusion"), (21, "Subtract"), (22, "Divide"),
(23, "Hue"), (24, "Saturation"), (25, "Color"), (26, "Luminosity"),
];
// Ensure texture is loaded
if self.texture.is_none() {
let color_image = egui::ColorImage::from_rgba_unmultiplied(
[self.width as usize, self.height as usize],
&self.blended,
);
self.texture = Some(ctx.load_texture("blended_preview", color_image, Default::default()));
}
let texture = self.texture.as_ref().unwrap().clone();
egui::CentralPanel::default().show(&ctx, |ui| {
ui.heading("HCIE Blend Engine Visual Tester");
ui.add_space(10.0);
ui.horizontal(|ui| {
// Left Column: Preview Canvas
ui.vertical(|ui| {
ui.image((texture.id(), texture.size_vec2()));
});
ui.add_space(20.0);
// Right Column: Controls
ui.vertical(|ui| {
let mut changed = false;
// Blend Mode Selection
ui.label("Blend Mode:");
let current_label = modes.iter().find(|m| m.0 == self.blend_mode).map(|m| m.1).unwrap_or("Normal");
egui::ComboBox::from_id_source("blend_mode_select")
.selected_text(current_label)
.show_ui(ui, |ui| {
for &(val, label) in &modes {
if ui.selectable_value(&mut self.blend_mode, val, label).clicked() {
changed = true;
}
}
});
ui.add_space(15.0);
// Opacity Slider
ui.label(format!("Opacity: {:.2}", self.opacity));
if ui.add(egui::Slider::new(&mut self.opacity, 0.0..=1.0)).changed() {
changed = true;
}
if changed {
self.update_blended();
}
ui.add_space(30.0);
ui.colored_label(egui::Color32::LIGHT_GREEN, "✓ Decoupled from hcie-protocol");
ui.colored_label(egui::Color32::LIGHT_GREEN, "✓ Compiles as a dynamic C FFI library");
ui.colored_label(egui::Color32::LIGHT_GREEN, "✓ Operating with parallel Rayon logic");
});
});
});
}
}
+316
View File
@@ -0,0 +1,316 @@
//! Color blending engine — 30+ blend modes.
use rand::random;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[repr(i32)]
pub enum BlendMode {
#[default]
Normal = 0,
Dissolve = 1,
Darken = 2,
Multiply = 3,
ColorBurn = 4,
LinearBurn = 5,
DarkerColor = 6,
Lighten = 7,
Screen = 8,
ColorDodge = 9,
LinearDodge = 10,
LighterColor = 11,
Overlay = 12,
SoftLight = 13,
HardLight = 14,
VividLight = 15,
LinearLight = 16,
PinLight = 17,
HardMix = 18,
Difference = 19,
Exclusion = 20,
Subtract = 21,
Divide = 22,
Hue = 23,
Saturation = 24,
Color = 25,
Luminosity = 26,
PassThrough = 27,
}
impl BlendMode {
pub const ALL: &'static [BlendMode] = &[
BlendMode::Normal,
BlendMode::Dissolve,
BlendMode::Darken,
BlendMode::Multiply,
BlendMode::ColorBurn,
BlendMode::LinearBurn,
BlendMode::DarkerColor,
BlendMode::Lighten,
BlendMode::Screen,
BlendMode::ColorDodge,
BlendMode::LinearDodge,
BlendMode::LighterColor,
BlendMode::Overlay,
BlendMode::SoftLight,
BlendMode::HardLight,
BlendMode::VividLight,
BlendMode::LinearLight,
BlendMode::PinLight,
BlendMode::HardMix,
BlendMode::Difference,
BlendMode::Exclusion,
BlendMode::Subtract,
BlendMode::Divide,
BlendMode::Hue,
BlendMode::Saturation,
BlendMode::Color,
BlendMode::Luminosity,
BlendMode::PassThrough,
];
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
pub enum Adjustment {
Curves {
lut_r: Vec<u8>,
lut_g: Vec<u8>,
lut_b: Vec<u8>,
},
GradientMap {
lut_r: Vec<u8>,
lut_g: Vec<u8>,
lut_b: Vec<u8>,
},
HueSaturation {
hue: i32,
saturation: i32,
lightness: i32,
},
}
fn rand_u8() -> u8 { random::<u8>() }
#[inline]
fn soft_light(d: f32, s: f32) -> f32 {
if s <= 0.5 {
d - (1.0 - 2.0 * s) * d * (1.0 - d)
} else {
d + (2.0 * s - 1.0) * (d.sqrt() - d)
}
}
#[inline]
fn pin_light(d: f32, s: f32) -> f32 {
if s < 0.5 { d.min(2.0 * s) } else { d.max(2.0 * (s - 0.5)) }
}
fn sat_lin(r: f32, g: f32, b: f32) -> f32 {
let max = r.max(g).max(b);
let min = r.min(g).min(b);
max - min
}
fn lum_lin(r: f32, g: f32, b: f32) -> f32 {
0.30 * r + 0.59 * g + 0.11 * b
}
fn set_sat(r: f32, g: f32, b: f32, s: f32) -> (f32, f32, f32) {
let cmax = r.max(g).max(b);
let cmin = r.min(g).min(b);
let old_sat = cmax - cmin;
if old_sat == 0.0 { return (0.0, 0.0, 0.0); }
let scale = s / old_sat;
(
(r - cmin) * scale,
(g - cmin) * scale,
(b - cmin) * scale,
)
}
fn set_lum(r: f32, g: f32, b: f32, l: f32) -> (f32, f32, f32) {
let d = l - lum_lin(r, g, b);
let mut r = r + d;
let mut g = g + d;
let mut b = b + d;
let min_val = r.min(g).min(b);
if min_val < 0.0 {
let l2 = lum_lin(r, g, b);
let s = if l2 == min_val { 1.0 } else { l2 / (l2 - min_val) };
r = l2 + (r - l2) * s;
g = l2 + (g - l2) * s;
b = l2 + (b - l2) * s;
}
let max_val = r.max(g).max(b);
if max_val > 1.0 {
let l2 = lum_lin(r, g, b);
let s = if l2 == max_val { 1.0 } else { (1.0 - l2) / (max_val - l2) };
r = l2 + (r - l2) * s;
g = l2 + (g - l2) * s;
b = l2 + (b - l2) * s;
}
(r, g, b)
}
pub fn blend_pixels(dst: [u8; 4], src: [u8; 4], mode: BlendMode, opacity: f32) -> [u8; 4] {
// Fast path: Normal mode + fully opaque src → return src directly
if mode == BlendMode::Normal && opacity >= 1.0 && src[3] == 255 {
return src;
}
let [dr, dg, db, da] = dst.map(|c| c as f32 / 255.0);
let [sr, sg, sb, sa] = src.map(|c| c as f32 / 255.0);
let eff_sa = sa * opacity;
let (br, bg, bb) = match mode {
BlendMode::Hue => {
let dst_sat = sat_lin(dr, dg, db);
let dst_lum = lum_lin(dr, dg, db);
let (sr2, sg2, sb2) = set_sat(sr, sg, sb, dst_sat);
set_lum(sr2, sg2, sb2, dst_lum)
}
BlendMode::Saturation => {
let src_sat = sat_lin(sr, sg, sb);
let dst_lum = lum_lin(dr, dg, db);
let (dr2, dg2, db2) = set_sat(dr, dg, db, src_sat);
set_lum(dr2, dg2, db2, dst_lum)
}
BlendMode::Color => {
let dst_lum = lum_lin(dr, dg, db);
set_lum(sr, sg, sb, dst_lum)
}
BlendMode::Luminosity => {
let src_lum = lum_lin(sr, sg, sb);
set_lum(dr, dg, db, src_lum)
}
BlendMode::Normal => (sr, sg, sb),
BlendMode::Dissolve => {
if opacity >= 1.0 && rand_u8() as f32 / 255.0 < eff_sa { (sr, sg, sb) } else { (dr, dg, db) }
}
BlendMode::Darken => {
(dr.min(sr), dg.min(sg), db.min(sb))
}
BlendMode::Multiply => {
(dr * sr, dg * sg, db * sb)
}
BlendMode::ColorBurn => {
let br = if sr <= 0.0 { 0.0 } else { 1.0 - ((1.0 - dr) / sr).min(1.0) };
let bg = if sg <= 0.0 { 0.0 } else { 1.0 - ((1.0 - dg) / sg).min(1.0) };
let bb = if sb <= 0.0 { 0.0 } else { 1.0 - ((1.0 - db) / sb).min(1.0) };
(br, bg, bb)
}
BlendMode::LinearBurn => {
((dr + sr - 1.0).max(0.0), (dg + sg - 1.0).max(0.0), (db + sb - 1.0).max(0.0))
}
BlendMode::DarkerColor => {
let d_lum = lum_lin(dr, dg, db);
let s_lum = lum_lin(sr, sg, sb);
if s_lum < d_lum { (sr, sg, sb) } else { (dr, dg, db) }
}
BlendMode::Lighten => {
(dr.max(sr), dg.max(sg), db.max(sb))
}
BlendMode::Screen => {
(1.0 - (1.0 - dr) * (1.0 - sr),
1.0 - (1.0 - dg) * (1.0 - sg),
1.0 - (1.0 - db) * (1.0 - sb))
}
BlendMode::ColorDodge => {
let br = if sr >= 1.0 { 1.0 } else { (dr / (1.0 - sr)).min(1.0) };
let bg = if sg >= 1.0 { 1.0 } else { (dg / (1.0 - sg)).min(1.0) };
let bb = if sb >= 1.0 { 1.0 } else { (db / (1.0 - sb)).min(1.0) };
(br, bg, bb)
}
BlendMode::LinearDodge => {
((dr + sr).min(1.0), (dg + sg).min(1.0), (db + sb).min(1.0))
}
BlendMode::LighterColor => {
let d_lum = lum_lin(dr, dg, db);
let s_lum = lum_lin(sr, sg, sb);
if s_lum > d_lum { (sr, sg, sb) } else { (dr, dg, db) }
}
BlendMode::Overlay => {
let br = if dr < 0.5 { 2.0 * dr * sr } else { 1.0 - 2.0 * (1.0 - dr) * (1.0 - sr) };
let bg = if dg < 0.5 { 2.0 * dg * sg } else { 1.0 - 2.0 * (1.0 - dg) * (1.0 - sg) };
let bb = if db < 0.5 { 2.0 * db * sb } else { 1.0 - 2.0 * (1.0 - db) * (1.0 - sb) };
(br, bg, bb)
}
BlendMode::SoftLight => {
(soft_light(dr, sr), soft_light(dg, sg), soft_light(db, sb))
}
BlendMode::HardLight => {
let br = if sr < 0.5 { 2.0 * dr * sr } else { 1.0 - 2.0 * (1.0 - dr) * (1.0 - sr) };
let bg = if sg < 0.5 { 2.0 * dg * sg } else { 1.0 - 2.0 * (1.0 - dg) * (1.0 - sg) };
let bb = if sb < 0.5 { 2.0 * db * sb } else { 1.0 - 2.0 * (1.0 - db) * (1.0 - sb) };
(br, bg, bb)
}
BlendMode::VividLight => {
let br = if sr > 0.5 { 1.0 - ((1.0 - dr) / (2.0 * (sr - 0.5))).min(1.0) } else { (dr / (1.0 - 2.0 * sr)).min(1.0) };
let bg = if sg > 0.5 { 1.0 - ((1.0 - dg) / (2.0 * (sg - 0.5))).min(1.0) } else { (dg / (1.0 - 2.0 * sg)).min(1.0) };
let bb = if sb > 0.5 { 1.0 - ((1.0 - db) / (2.0 * (sb - 0.5))).min(1.0) } else { (db / (1.0 - 2.0 * sb)).min(1.0) };
(br, bg, bb)
}
BlendMode::LinearLight => {
let br = if sr > 0.5 { (dr + 2.0 * (sr - 0.5)).clamp(0.0, 1.0) } else { (dr + 2.0 * sr - 1.0).clamp(0.0, 1.0) };
let bg = if sg > 0.5 { (dg + 2.0 * (sg - 0.5)).clamp(0.0, 1.0) } else { (dg + 2.0 * sg - 1.0).clamp(0.0, 1.0) };
let bb = if sb > 0.5 { (db + 2.0 * (sb - 0.5)).clamp(0.0, 1.0) } else { (db + 2.0 * sb - 1.0).clamp(0.0, 1.0) };
(br, bg, bb)
}
BlendMode::PinLight => {
(pin_light(dr, sr), pin_light(dg, sg), pin_light(db, sb))
}
BlendMode::HardMix => {
let br = if sr <= 1.0 - dr { 0.0 } else { 1.0 };
let bg = if sg <= 1.0 - dg { 0.0 } else { 1.0 };
let bb = if sb <= 1.0 - db { 0.0 } else { 1.0 };
(br, bg, bb)
}
BlendMode::Difference => {
((dr - sr).abs(), (dg - sg).abs(), (db - sb).abs())
}
BlendMode::Exclusion => {
(dr + sr - 2.0 * dr * sr, dg + sg - 2.0 * dg * sg, db + sb - 2.0 * db * sb)
}
BlendMode::Subtract => {
((dr - sr).max(0.0), (dg - sg).max(0.0), (db - sb).max(0.0))
}
BlendMode::Divide => {
let br = if sr == 0.0 { 1.0 } else { (dr / sr).min(1.0) };
let bg = if sg == 0.0 { 1.0 } else { (dg / sg).min(1.0) };
let bb = if sb == 0.0 { 1.0 } else { (db / sb).min(1.0) };
(br, bg, bb)
}
BlendMode::PassThrough => {
panic!("PassThrough is a group isolation mode, not a per-pixel blend mode")
}
};
// Standard W3C / Porter-Duff "src-over" alpha compositing on top of the blended color.
let out_a = eff_sa + da * (1.0 - eff_sa);
if out_a < 1e-6 { return [0, 0, 0, 0]; }
let out_r = ((1.0 - eff_sa) * da * dr + (1.0 - da) * eff_sa * sr + da * eff_sa * br) / out_a;
let out_g = ((1.0 - eff_sa) * da * dg + (1.0 - da) * eff_sa * sg + da * eff_sa * bg) / out_a;
let out_b = ((1.0 - eff_sa) * da * db + (1.0 - da) * eff_sa * sb + da * eff_sa * bb) / out_a;
[
(out_r * 255.0 + 0.5) as u8,
(out_g * 255.0 + 0.5) as u8,
(out_b * 255.0 + 0.5) as u8,
(out_a * 255.0 + 0.5) as u8,
]
}
pub fn blend_buffers(dst: &mut [u8], src: &[u8], mode: BlendMode, opacity: f32) {
use rayon::prelude::*;
dst.par_chunks_exact_mut(4)
.zip(src.par_chunks_exact(4))
.for_each(|(d_pixel, s_pixel)| {
let blended = blend_pixels(
[d_pixel[0], d_pixel[1], d_pixel[2], d_pixel[3]],
[s_pixel[0], s_pixel[1], s_pixel[2], s_pixel[3]],
mode,
opacity,
);
d_pixel.copy_from_slice(&blended);
});
}
@@ -0,0 +1,7 @@
# Seeds for failure cases proptest has generated in the past. It is
# automatically read and these particular cases re-run before any
# novel cases are generated.
#
# It is recommended to check this file in to source control so that
# everyone who runs the test benefits from these saved cases.
cc 68c12424c03105101a57a32cca62a818d8bea787f9f55785def9fa3e2e762b29 # shrinks to dr = 0, dg = 0, db = 0, da = 0, sr = 0, sg = 0, sb = 0, sa = 0
+97
View File
@@ -0,0 +1,97 @@
#![allow(dead_code, unused_imports, unused_variables, unused_macros, unreachable_patterns, unused_assignments, clippy::cfg)]
use hcie_blend::BlendMode;
#[test]
fn test_normal_zero_opacity_identity() {
let dst = [128, 64, 32, 200];
let src = [255, 0, 0, 255];
let result = hcie_blend::blend_pixels(dst, src, BlendMode::Normal, 0.0);
assert_eq!(result, dst, "opacity=0 should return dst unchanged");
}
#[test]
fn test_normal_full_opacity() {
let dst = [100, 100, 100, 255];
let src = [200, 50, 80, 255];
let result = hcie_blend::blend_pixels(dst, src, BlendMode::Normal, 1.0);
assert_eq!(result, src, "Normal+opacity=1 should output src");
}
#[test]
fn test_opacity_zero_no_mutation_for_all_modes() {
let dst = [128, 64, 32, 255];
let src = [255, 0, 0, 255];
for mode in BlendMode::ALL {
if *mode == BlendMode::PassThrough { continue; }
let result = hcie_blend::blend_pixels(dst, src, *mode, 0.0);
assert_eq!(result, dst, "opacity=0 should return dst unchanged for {:?}", mode);
}
}
#[rstest::rstest]
#[case(BlendMode::Multiply, [200, 100, 50, 255], [100, 100, 100, 255])]
#[case(BlendMode::Screen, [200, 100, 50, 255], [100, 100, 100, 255])]
#[case(BlendMode::Difference, [100, 200, 50, 255], [100, 150, 150, 255])]
#[case(BlendMode::Darken, [100, 200, 50, 255], [100, 100, 150, 255])]
#[case(BlendMode::Lighten, [100, 200, 50, 255], [150, 200, 150, 255])]
fn test_specific_blend_modes(
#[case] mode: BlendMode,
#[case] src: [u8; 4],
#[case] dst: [u8; 4],
) {
let result = hcie_blend::blend_pixels(dst, src, mode, 1.0);
assert!(
result[3] > 0 || (src[3] == 0 && dst[3] == 0),
"Blend result should have valid alpha for {:?}, got {:?}",
mode, result
);
}
#[test]
fn test_transparent_src_normal() {
let dst = [128, 64, 32, 200];
let src = [0, 0, 0, 0];
let result = hcie_blend::blend_pixels(dst, src, BlendMode::Normal, 1.0);
assert_eq!(result, dst, "fully transparent src should leave dst unchanged");
}
#[test]
fn test_all_modes_produce_valid_alpha() {
let dst = [100, 150, 200, 255];
let src = [50, 100, 200, 128];
for mode in BlendMode::ALL {
if *mode == BlendMode::PassThrough { continue; }
let _result = hcie_blend::blend_pixels(dst, src, *mode, 0.5);
// u8 values are always valid (0-255)
}
}
#[test]
fn test_multiply_darkens() {
let dst = [200, 200, 200, 255];
let src = [100, 100, 100, 255];
let result = hcie_blend::blend_pixels(dst, src, BlendMode::Multiply, 1.0);
assert!(result[0] <= 100, "Multiply should darken red channel");
}
#[test]
fn test_screen_lightens() {
let dst = [100, 100, 100, 255];
let src = [200, 200, 200, 255];
let result = hcie_blend::blend_pixels(dst, src, BlendMode::Screen, 1.0);
assert!(result[0] > 100, "Screen should lighten red channel");
}
proptest::proptest! {
#[test]
fn identity_at_zero_opacity(dr: u8, dg: u8, db: u8, da: u8,
sr: u8, sg: u8, sb: u8, sa: u8) {
let dst = [dr, dg, db, if da == 0 { 1 } else { da }];
let src = [sr, sg, sb, if sa == 0 { 1 } else { sa }];
for mode in BlendMode::ALL {
if *mode == BlendMode::PassThrough { continue; }
let result = hcie_blend::blend_pixels(dst, src, *mode, 0.0);
proptest::prop_assert_eq!(result, dst, "opacity=0 identity failed for {:?}", mode);
}
}
}