diff --git a/rust/Cargo.toml b/rust/Cargo.toml new file mode 100644 index 0000000..4d98661 --- /dev/null +++ b/rust/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "mixbox" +version = "2.0.0" +edition = "2021" +description = "Pigment-Based Color Mixing" +readme = "README.md" +homepage = "https://scrtwpns.com/mixbox" +repository = "https://github.com/scrtwpns/mixbox" +license = "CC-BY-NC-4.0" +keywords = ["color-mixing", "pigments", "rgb", "kubelka-munk", "paints"] + +[dependencies] +libm = "0.2.5" diff --git a/rust/README.md b/rust/README.md new file mode 100644 index 0000000..4ce0e2c --- /dev/null +++ b/rust/README.md @@ -0,0 +1,56 @@ +# Mixbox for Rust +```ini +mixbox = "2.0.0" # add this line to your Cargo.toml +``` + +## Usage +```rust +fn main() { + let rgb1 = [0, 33, 133]; // blue + let rgb2 = [252, 211, 0]; // yellow + let t = 0.5; // mixing ratio + + let [r, g, b] = mixbox::lerp(&rgb1, &rgb2, t); + + println!("{} {} {}", r, g, b); +} +``` + +## Mixing Multiple Colors +```rust +let z1 = mixbox::rgb_to_latent(&rgb1); +let z2 = mixbox::rgb_to_latent(&rgb2); +let z3 = mixbox::rgb_to_latent(&rgb3); + +let mut z_mix = [0.0; mixbox::LATENT_SIZE]; + +for i in 0..z_mix.len() { // mix together: + z_mix[i] = 0.3*z1[i] + // 30% of rgb1 + 0.6*z2[i] + // 60% of rgb2 + 0.1*z3[i]; // 10% of rgb3 +} + +let rgb_mix = mixbox::latent_to_rgb(&z_mix); +``` + +## Pigment Colors +| Pigment | | RGB | Float RGB | Linear RGB | +| --- | --- |:----:|:----:|:----:| +| Cadmium Yellow | | 254, 236, 0 | 0.996, 0.925, 0.0 | 0.991, 0.839, 0.0 | +| Hansa Yellow | | 252, 211, 0 | 0.988, 0.827, 0.0 | 0.973, 0.651, 0.0 | +| Cadmium Orange | | 255, 105, 0 | 1.0, 0.412, 0.0 | 1.0, 0.141, 0.0 | +| Cadmium Red | | 255, 39, 2 | 1.0, 0.153, 0.008 | 1.0, 0.02, 0.001 | +| Quinacridone Magenta | | 128, 2, 46 | 0.502, 0.008, 0.18 | 0.216, 0.001, 0.027 | +| Cobalt Violet | | 78, 0, 66 | 0.306, 0.0, 0.259 | 0.076, 0.0, 0.054 | +| Ultramarine Blue | | 25, 0, 89 | 0.098, 0.0, 0.349 | 0.01, 0.0, 0.1 | +| Cobalt Blue | | 0, 33, 133 | 0.0, 0.129, 0.522 | 0.0, 0.015, 0.235 | +| Phthalo Blue | | 13, 27, 68 | 0.051, 0.106, 0.267 | 0.004, 0.011, 0.058 | +| Phthalo Green | | 0, 60, 50 | 0.0, 0.235, 0.196 | 0.0, 0.045, 0.032 | +| Permanent Green | | 7, 109, 22 | 0.027, 0.427, 0.086 | 0.002, 0.153, 0.008 | +| Sap Green | | 107, 148, 4 | 0.42, 0.58, 0.016 | 0.147, 0.296, 0.001 | +| Burnt Sienna | | 123, 72, 0 | 0.482, 0.282, 0.0 | 0.198, 0.065, 0.0 | + +## License +Copyright (c) 2022, Secret Weapons. All rights reserved.
+Mixbox is provided under the CC BY-NC 4.0 license for non-commercial use only.
+If you want to obtain commercial license, please contact: mixbox@scrtwpns.com diff --git a/rust/src/lib.rs b/rust/src/lib.rs new file mode 100644 index 0000000..9564cd6 --- /dev/null +++ b/rust/src/lib.rs @@ -0,0 +1,258 @@ +// ========================================================== +// MIXBOX 2.0 (c) 2022 Secret Weapons. All rights reserved. +// License: Creative Commons Attribution-NonCommercial 4.0 +// Authors: Sarka Sochorova and Ondrej Jamriska +// ========================================================== +// +// BASIC USAGE +// +// let rgb_mix = mixbox::lerp(&rgb1, &rgb2, t); +// +// MULTI-COLOR MIXING +// +// let z1 = mixbox::rgb_to_latent(&rgb1); +// let z2 = mixbox::rgb_to_latent(&rgb2); +// let z3 = mixbox::rgb_to_latent(&rgb3); +// +// let mut z_mix = [0.0; mixbox::LATENT_SIZE]; +// +// for i in 0..z_mix.len() { // mix together: +// z_mix[i] = 0.3*z1[i] + // 30% of rgb1 +// 0.6*z2[i] + // 60% of rgb2 +// 0.1*z3[i]; // 10% of rgb3 +// } +// +// let rgb_mix = mixbox::latent_to_rgb(&z_mix); +// +// PIGMENT COLORS +// +// Cadmium Yellow 254, 236, 0 +// Hansa Yellow 252, 211, 0 +// Cadmium Orange 255, 105, 0 +// Cadmium Red 255, 39, 2 +// Quinacridone Magenta 128, 2, 46 +// Cobalt Violet 78, 0, 66 +// Ultramarine Blue 25, 0, 89 +// Cobalt Blue 0, 33, 133 +// Phthalo Blue 13, 27, 68 +// Phthalo Green 0, 60, 50 +// Permanent Green 7, 109, 22 +// Sap Green 107, 148, 4 +// Burnt Sienna 123, 72, 0 +// +// LICENSING +// +// If you want to obtain commercial license, please +// contact us at: mixbox@scrtwpns.com +// + +#![no_std] + +use libm::powf; + +pub const LATENT_SIZE: usize = 7; + +const MIXBOX_LUT : &[u8] = include_bytes!("lut.dat"); + +#[inline(always)] +fn clamp01(x: f32) -> f32 { + if x < 0.0 { + 0.0 + } else if x > 1.0 { + 1.0 + } else { + x + } +} + +#[inline(always)] +fn srgb_to_linear(x: f32) -> f32 { + if x >= 0.04045 { + powf((x + 0.055) / 1.055, 2.4) + } else { + x / 12.92 + } +} + +#[inline(always)] +fn linear_to_srgb(x: f32) -> f32 { + if x >= 0.0031308 { + 1.055 * powf(x, 1.0 / 2.4) - 0.055 + } else { + 12.92 * x + } +} + +#[inline(always)] +fn eval_polynomial(c0: f32, c1: f32, c2: f32, c3: f32) -> [f32; 3] { + let c00 = c0 * c0; + let c11 = c1 * c1; + let c22 = c2 * c2; + let c33 = c3 * c3; + let c01 = c0 * c1; + let c02 = c0 * c2; + let c12 = c1 * c2; + + let mut r = 0.0; + let mut g = 0.0; + let mut b = 0.0; + + let w00 = c0 * c00; r += 0.07717053*w00; g += 0.02826978*w00; b += 0.24832992*w00; + let w01 = c1 * c11; r += 0.95912302*w01; g += 0.80256528*w01; b += 0.03561839*w01; + let w02 = c2 * c22; r += 0.74683774*w02; g += 0.04868586*w02; b += 0.00000000*w02; + let w03 = c3 * c33; r += 0.99518138*w03; g += 0.99978149*w03; b += 0.99704802*w03; + let w04 = c00 * c1; r += 0.04819146*w04; g += 0.83363781*w04; b += 0.32515377*w04; + let w05 = c01 * c1; r += -0.68146950*w05; g += 1.46107803*w05; b += 1.06980936*w05; + let w06 = c00 * c2; r += 0.27058419*w06; g += -0.15324870*w06; b += 1.98735057*w06; + let w07 = c02 * c2; r += 0.80478189*w07; g += 0.67093710*w07; b += 0.18424500*w07; + let w08 = c00 * c3; r += -0.35031003*w08; g += 1.37855826*w08; b += 3.68865000*w08; + let w09 = c0 * c33; r += 1.05128046*w09; g += 1.97815239*w09; b += 2.82989073*w09; + let w10 = c11 * c2; r += 3.21607125*w10; g += 0.81270228*w10; b += 1.03384539*w10; + let w11 = c1 * c22; r += 2.78893374*w11; g += 0.41565549*w11; b += -0.04487295*w11; + let w12 = c11 * c3; r += 3.02162577*w12; g += 2.55374103*w12; b += 0.32766114*w12; + let w13 = c1 * c33; r += 2.95124691*w13; g += 2.81201112*w13; b += 1.17578442*w13; + let w14 = c22 * c3; r += 2.82677043*w14; g += 0.79933038*w14; b += 1.81715262*w14; + let w15 = c2 * c33; r += 2.99691099*w15; g += 1.22593053*w15; b += 1.80653661*w15; + let w16 = c01 * c2; r += 1.87394106*w16; g += 2.05027182*w16; b += -0.29835996*w16; + let w17 = c01 * c3; r += 2.56609566*w17; g += 7.03428198*w17; b += 0.62575374*w17; + let w18 = c02 * c3; r += 4.08329484*w18; g += -1.40408358*w18; b += 2.14995522*w18; + let w19 = c12 * c3; r += 6.00078678*w19; g += 2.55552042*w19; b += 1.90739502*w19; + + [r, g, b] +} + +pub fn float_rgb_to_latent(rgb: &[f32; 3]) -> [f32; LATENT_SIZE] { + let r01 = clamp01(rgb[0]); + let g01 = clamp01(rgb[1]); + let b01 = clamp01(rgb[2]); + + let x = r01 * 63.0; + let y = g01 * 63.0; + let z = b01 * 63.0; + + let ix = x as i32; + let iy = y as i32; + let iz = z as i32; + + let tx = x - (ix as f32); + let ty = y - (iy as f32); + let tz = z - (iz as f32); + + let lut = &MIXBOX_LUT[(((ix + iy*64 + iz*64*64) & 0x3FFFF) * 3) as usize ..]; + + let mut c0 = 0.0; + let mut c1 = 0.0; + let mut c2 = 0.0; + + let w0 = (1.0-tx)*(1.0-ty)*(1.0-tz); c0 += w0*(lut[ 192] as f32); c1 += w0*(lut[ 193] as f32); c2 += w0*(lut[ 194] as f32); + let w1 = ( tx)*(1.0-ty)*(1.0-tz); c0 += w1*(lut[ 195] as f32); c1 += w1*(lut[ 196] as f32); c2 += w1*(lut[ 197] as f32); + let w2 = (1.0-tx)*( ty)*(1.0-tz); c0 += w2*(lut[ 384] as f32); c1 += w2*(lut[ 385] as f32); c2 += w2*(lut[ 386] as f32); + let w3 = ( tx)*( ty)*(1.0-tz); c0 += w3*(lut[ 387] as f32); c1 += w3*(lut[ 388] as f32); c2 += w3*(lut[ 389] as f32); + let w4 = (1.0-tx)*(1.0-ty)*( tz); c0 += w4*(lut[12480] as f32); c1 += w4*(lut[12481] as f32); c2 += w4*(lut[12482] as f32); + let w5 = ( tx)*(1.0-ty)*( tz); c0 += w5*(lut[12483] as f32); c1 += w5*(lut[12484] as f32); c2 += w5*(lut[12485] as f32); + let w6 = (1.0-tx)*( ty)*( tz); c0 += w6*(lut[12672] as f32); c1 += w6*(lut[12673] as f32); c2 += w6*(lut[12674] as f32); + let w7 = ( tx)*( ty)*( tz); c0 += w7*(lut[12675] as f32); c1 += w7*(lut[12676] as f32); c2 += w7*(lut[12677] as f32); + + c0 *= 1.0 / 255.0; + c1 *= 1.0 / 255.0; + c2 *= 1.0 / 255.0; + + let c3 = 1.0 - (c0 + c1 + c2); + + let mixrgb = eval_polynomial(c0, c1, c2, c3); + + [ + c0, + c1, + c2, + c3, + r01 - mixrgb[0], + g01 - mixrgb[1], + b01 - mixrgb[2], + ] +} + +pub fn latent_to_float_rgb(latent: &[f32; LATENT_SIZE]) -> [f32; 3] { + let rgb = eval_polynomial(latent[0], latent[1], latent[2], latent[3]); + + [ + clamp01(rgb[0] + latent[4]), + clamp01(rgb[1] + latent[5]), + clamp01(rgb[2] + latent[6]), + ] +} + +pub fn latent_to_rgb(latent: &[f32; LATENT_SIZE]) -> [u8; 3] { + let rgb = latent_to_float_rgb(latent); + + [ + (rgb[0] * 255.0 + 0.5) as u8, + (rgb[1] * 255.0 + 0.5) as u8, + (rgb[2] * 255.0 + 0.5) as u8, + ] +} + +pub fn rgb_to_latent(rgb: &[u8; 3]) -> [f32; LATENT_SIZE] { + float_rgb_to_latent(&[ + (rgb[0] as f32) / 255.0, + (rgb[1] as f32) / 255.0, + (rgb[2] as f32) / 255.0 + ]) +} + +pub fn linear_float_rgb_to_latent(rgb: &[f32; 3]) -> [f32; LATENT_SIZE] { + float_rgb_to_latent(&[ + linear_to_srgb(rgb[0]), + linear_to_srgb(rgb[1]), + linear_to_srgb(rgb[2]), + ]) +} + +pub fn latent_to_linear_float_rgb(latent: &[f32; LATENT_SIZE]) -> [f32; 3] { + let rgb = latent_to_float_rgb(latent); + + [ + srgb_to_linear(rgb[0]), + srgb_to_linear(rgb[1]), + srgb_to_linear(rgb[2]), + ] +} + +pub fn lerp(rgb1: &[u8; 3], rgb2: &[u8; 3], t: f32) -> [u8; 3] { + let latent1 = rgb_to_latent(rgb1); + let latent2 = rgb_to_latent(rgb2); + + let mut latent_mix = [0.0; LATENT_SIZE]; + + for i in 0..latent_mix.len() { + latent_mix[i] = (1.0 - t) * latent1[i] + t * latent2[i]; + } + + latent_to_rgb(&latent_mix) +} + +pub fn lerp_float(rgb1: &[f32; 3], rgb2: &[f32; 3], t: f32) -> [f32; 3] { + let latent1 = float_rgb_to_latent(rgb1); + let latent2 = float_rgb_to_latent(rgb2); + + let mut latent_mix = [0.0; LATENT_SIZE]; + + for i in 0..latent_mix.len() { + latent_mix[i] = (1.0 - t) * latent1[i] + t * latent2[i]; + } + + latent_to_float_rgb(&latent_mix) +} + +pub fn lerp_linear_float(rgb1: &[f32; 3], rgb2: &[f32; 3], t: f32) -> [f32; 3] { + let latent1 = linear_float_rgb_to_latent(rgb1); + let latent2 = linear_float_rgb_to_latent(rgb2); + + let mut latent_mix = [0.0; LATENT_SIZE]; + + for i in 0..latent_mix.len() { + latent_mix[i] = (1.0 - t) * latent1[i] + t * latent2[i]; + } + + latent_to_linear_float_rgb(&latent_mix) +} diff --git a/rust/src/lut.dat b/rust/src/lut.dat new file mode 100644 index 0000000..0c7dcd2 Binary files /dev/null and b/rust/src/lut.dat differ