use crate::canvas::Rgb;
use crate::error::{CanvasError, WasmError, WasmResult};
use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct PaletteEntry {
pub value: f64,
pub color: Rgb,
}
impl PaletteEntry {
pub const fn new(value: f64, color: Rgb) -> Self {
Self { value, color }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ColorPalette {
pub name: String,
pub entries: Vec<PaletteEntry>,
}
impl ColorPalette {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
entries: Vec::new(),
}
}
pub fn add_entry(&mut self, value: f64, color: Rgb) {
self.entries.push(PaletteEntry::new(value, color));
self.entries.sort_by(|a, b| {
a.value
.partial_cmp(&b.value)
.unwrap_or(std::cmp::Ordering::Equal)
});
}
pub fn interpolate(&self, value: f64) -> Option<Rgb> {
if self.entries.is_empty() {
return None;
}
let value = value.clamp(0.0, 1.0);
let mut lower = None;
let mut upper = None;
for entry in &self.entries {
if entry.value <= value {
lower = Some(entry);
}
if entry.value >= value && upper.is_none() {
upper = Some(entry);
}
}
match (lower, upper) {
(Some(l), Some(u)) if (l.value - u.value).abs() < f64::EPSILON => Some(l.color),
(Some(l), Some(u)) => {
let t = (value - l.value) / (u.value - l.value);
Some(interpolate_rgb(l.color, u.color, t))
}
(Some(l), None) => Some(l.color),
(None, Some(u)) => Some(u.color),
(None, None) => None,
}
}
pub fn apply_to_grayscale(&self, data: &mut [u8]) -> WasmResult<()> {
for chunk in data.chunks_exact_mut(4) {
let gray = chunk[0];
let value = f64::from(gray) / 255.0;
if let Some(color) = self.interpolate(value) {
chunk[0] = color.r;
chunk[1] = color.g;
chunk[2] = color.b;
}
}
Ok(())
}
pub fn grayscale() -> Self {
let mut palette = Self::new("grayscale");
palette.add_entry(0.0, Rgb::new(0, 0, 0));
palette.add_entry(1.0, Rgb::new(255, 255, 255));
palette
}
pub fn viridis() -> Self {
let mut palette = Self::new("viridis");
palette.add_entry(0.0, Rgb::new(68, 1, 84));
palette.add_entry(0.25, Rgb::new(59, 82, 139));
palette.add_entry(0.5, Rgb::new(33, 145, 140));
palette.add_entry(0.75, Rgb::new(94, 201, 98));
palette.add_entry(1.0, Rgb::new(253, 231, 37));
palette
}
pub fn plasma() -> Self {
let mut palette = Self::new("plasma");
palette.add_entry(0.0, Rgb::new(13, 8, 135));
palette.add_entry(0.25, Rgb::new(126, 3, 168));
palette.add_entry(0.5, Rgb::new(204, 71, 120));
palette.add_entry(0.75, Rgb::new(248, 149, 64));
palette.add_entry(1.0, Rgb::new(240, 249, 33));
palette
}
pub fn inferno() -> Self {
let mut palette = Self::new("inferno");
palette.add_entry(0.0, Rgb::new(0, 0, 4));
palette.add_entry(0.25, Rgb::new(87, 16, 110));
palette.add_entry(0.5, Rgb::new(188, 55, 84));
palette.add_entry(0.75, Rgb::new(249, 142, 9));
palette.add_entry(1.0, Rgb::new(252, 255, 164));
palette
}
pub fn terrain() -> Self {
let mut palette = Self::new("terrain");
palette.add_entry(0.0, Rgb::new(0, 0, 128));
palette.add_entry(0.2, Rgb::new(0, 128, 255));
palette.add_entry(0.4, Rgb::new(0, 255, 0));
palette.add_entry(0.6, Rgb::new(255, 255, 0));
palette.add_entry(0.8, Rgb::new(165, 82, 42));
palette.add_entry(1.0, Rgb::new(255, 255, 255));
palette
}
pub fn rainbow() -> Self {
let mut palette = Self::new("rainbow");
palette.add_entry(0.0, Rgb::new(255, 0, 0));
palette.add_entry(0.2, Rgb::new(255, 165, 0));
palette.add_entry(0.4, Rgb::new(255, 255, 0));
palette.add_entry(0.6, Rgb::new(0, 255, 0));
palette.add_entry(0.8, Rgb::new(0, 0, 255));
palette.add_entry(1.0, Rgb::new(128, 0, 128));
palette
}
pub fn rdylbu() -> Self {
let mut palette = Self::new("rdylbu");
palette.add_entry(0.0, Rgb::new(165, 0, 38));
palette.add_entry(0.25, Rgb::new(244, 109, 67));
palette.add_entry(0.5, Rgb::new(255, 255, 191));
palette.add_entry(0.75, Rgb::new(116, 173, 209));
palette.add_entry(1.0, Rgb::new(49, 54, 149));
palette
}
}
fn interpolate_rgb(a: Rgb, b: Rgb, t: f64) -> Rgb {
let t = t.clamp(0.0, 1.0);
Rgb::new(
((1.0 - t) * f64::from(a.r) + t * f64::from(b.r)) as u8,
((1.0 - t) * f64::from(a.g) + t * f64::from(b.g)) as u8,
((1.0 - t) * f64::from(a.b) + t * f64::from(b.b)) as u8,
)
}
pub struct GradientGenerator {
start: Rgb,
end: Rgb,
steps: usize,
}
impl GradientGenerator {
pub const fn new(start: Rgb, end: Rgb, steps: usize) -> Self {
Self { start, end, steps }
}
pub fn generate(&self) -> Vec<Rgb> {
if self.steps == 0 {
return Vec::new();
}
if self.steps == 1 {
return vec![self.start];
}
let mut gradient = Vec::with_capacity(self.steps);
for i in 0..self.steps {
let t = i as f64 / (self.steps - 1) as f64;
gradient.push(interpolate_rgb(self.start, self.end, t));
}
gradient
}
pub fn generate_multi_stop(colors: &[Rgb], steps: usize) -> Vec<Rgb> {
if colors.is_empty() || steps == 0 {
return Vec::new();
}
if colors.len() == 1 {
return vec![colors[0]; steps];
}
let mut result = Vec::with_capacity(steps);
let segment_steps = steps / (colors.len() - 1);
for i in 0..colors.len() - 1 {
let gradient_gen = Self::new(colors[i], colors[i + 1], segment_steps);
result.extend(gradient_gen.generate());
}
while result.len() < steps {
result.push(*colors.last().expect("colors is not empty"));
}
result.truncate(steps);
result
}
}
#[derive(Debug, Clone, Copy)]
pub struct ColorCorrectionMatrix {
matrix: [[f64; 3]; 3],
}
impl ColorCorrectionMatrix {
pub const fn new(matrix: [[f64; 3]; 3]) -> Self {
Self { matrix }
}
pub const fn identity() -> Self {
Self::new([[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]])
}
pub fn brightness(factor: f64) -> Self {
Self::new([[factor, 0.0, 0.0], [0.0, factor, 0.0], [0.0, 0.0, factor]])
}
pub fn contrast(factor: f64) -> Self {
let _t = (1.0 - factor) / 2.0;
Self::new([[factor, 0.0, 0.0], [0.0, factor, 0.0], [0.0, 0.0, factor]])
}
pub fn saturation(factor: f64) -> Self {
let lum_r = 0.3086;
let lum_g = 0.6094;
let lum_b = 0.0820;
let sr = (1.0 - factor) * lum_r;
let sg = (1.0 - factor) * lum_g;
let sb = (1.0 - factor) * lum_b;
Self::new([
[sr + factor, sr, sr],
[sg, sg + factor, sg],
[sb, sb, sb + factor],
])
}
pub fn apply(&self, color: Rgb) -> Rgb {
let r = f64::from(color.r);
let g = f64::from(color.g);
let b = f64::from(color.b);
let new_r = (self.matrix[0][0] * r + self.matrix[0][1] * g + self.matrix[0][2] * b)
.clamp(0.0, 255.0);
let new_g = (self.matrix[1][0] * r + self.matrix[1][1] * g + self.matrix[1][2] * b)
.clamp(0.0, 255.0);
let new_b = (self.matrix[2][0] * r + self.matrix[2][1] * g + self.matrix[2][2] * b)
.clamp(0.0, 255.0);
Rgb::new(new_r as u8, new_g as u8, new_b as u8)
}
pub fn apply_to_image(&self, data: &mut [u8]) {
for chunk in data.chunks_exact_mut(4) {
let color = Rgb::new(chunk[0], chunk[1], chunk[2]);
let corrected = self.apply(color);
chunk[0] = corrected.r;
chunk[1] = corrected.g;
chunk[2] = corrected.b;
}
}
pub fn compose(&self, other: &Self) -> Self {
let mut result = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
for k in 0..3 {
result[i][j] += self.matrix[i][k] * other.matrix[k][j];
}
}
}
Self::new(result)
}
}
pub struct ColorTemperature;
impl ColorTemperature {
pub fn adjust(color: Rgb, temperature: f64) -> Rgb {
let temp = temperature.clamp(-1.0, 1.0);
let r = if temp > 0.0 {
(f64::from(color.r) + 255.0 * temp).min(255.0) as u8
} else {
color.r
};
let b = if temp < 0.0 {
(f64::from(color.b) + 255.0 * (-temp)).min(255.0) as u8
} else {
color.b
};
Rgb::new(r, color.g, b)
}
pub fn adjust_image(data: &mut [u8], temperature: f64) {
for chunk in data.chunks_exact_mut(4) {
let color = Rgb::new(chunk[0], chunk[1], chunk[2]);
let adjusted = Self::adjust(color, temperature);
chunk[0] = adjusted.r;
chunk[1] = adjusted.g;
chunk[2] = adjusted.b;
}
}
}
pub struct WhiteBalance;
impl WhiteBalance {
pub fn auto_gray_world(data: &mut [u8], width: u32, height: u32) -> WasmResult<()> {
let pixel_count = (width * height) as usize;
let mut r_sum = 0u64;
let mut g_sum = 0u64;
let mut b_sum = 0u64;
for chunk in data.chunks_exact(4).take(pixel_count) {
r_sum += u64::from(chunk[0]);
g_sum += u64::from(chunk[1]);
b_sum += u64::from(chunk[2]);
}
let r_avg = r_sum as f64 / pixel_count as f64;
let g_avg = g_sum as f64 / pixel_count as f64;
let b_avg = b_sum as f64 / pixel_count as f64;
let gray = (r_avg + g_avg + b_avg) / 3.0;
if gray < 1.0 {
return Ok(());
}
let r_gain = gray / r_avg;
let g_gain = gray / g_avg;
let b_gain = gray / b_avg;
for chunk in data.chunks_exact_mut(4) {
chunk[0] = (f64::from(chunk[0]) * r_gain).min(255.0) as u8;
chunk[1] = (f64::from(chunk[1]) * g_gain).min(255.0) as u8;
chunk[2] = (f64::from(chunk[2]) * b_gain).min(255.0) as u8;
}
Ok(())
}
}
pub struct ColorQuantizer;
impl ColorQuantizer {
pub fn quantize(data: &mut [u8], num_colors: usize) -> WasmResult<()> {
if num_colors == 0 {
return Err(WasmError::Canvas(CanvasError::InvalidDimensions {
width: 0,
height: 0,
reason: "Number of colors must be greater than 0".to_string(),
}));
}
let step = 256 / num_colors;
for chunk in data.chunks_exact_mut(4) {
chunk[0] = ((chunk[0] as usize / step) * step) as u8;
chunk[1] = ((chunk[1] as usize / step) * step) as u8;
chunk[2] = ((chunk[2] as usize / step) * step) as u8;
}
Ok(())
}
pub fn posterize(data: &mut [u8], levels: u8) {
if levels == 0 {
return;
}
let step = 256 / levels as usize;
for chunk in data.chunks_exact_mut(4) {
for i in 0..3 {
let value = chunk[i] as usize;
chunk[i] = ((value / step) * step) as u8;
}
}
}
}
pub struct ChannelOps;
impl ChannelOps {
pub fn swap_channels(data: &mut [u8], channel_a: usize, channel_b: usize) {
if channel_a >= 3 || channel_b >= 3 {
return;
}
for chunk in data.chunks_exact_mut(4) {
chunk.swap(channel_a, channel_b);
}
}
pub fn extract_channel(data: &[u8], channel: usize) -> Vec<u8> {
if channel >= 3 {
return Vec::new();
}
let mut result = Vec::with_capacity(data.len());
for chunk in data.chunks_exact(4) {
let value = chunk[channel];
result.extend_from_slice(&[value, value, value, 255]);
}
result
}
pub fn mix_channels(data: &mut [u8], r_mix: [f64; 3], g_mix: [f64; 3], b_mix: [f64; 3]) {
for chunk in data.chunks_exact_mut(4) {
let r = f64::from(chunk[0]);
let g = f64::from(chunk[1]);
let b = f64::from(chunk[2]);
let new_r = (r * r_mix[0] + g * r_mix[1] + b * r_mix[2]).clamp(0.0, 255.0);
let new_g = (r * g_mix[0] + g * g_mix[1] + b * g_mix[2]).clamp(0.0, 255.0);
let new_b = (r * b_mix[0] + g * b_mix[1] + b * b_mix[2]).clamp(0.0, 255.0);
chunk[0] = new_r as u8;
chunk[1] = new_g as u8;
chunk[2] = new_b as u8;
}
}
}
#[wasm_bindgen]
pub struct WasmColorPalette {
palette: ColorPalette,
}
#[wasm_bindgen]
impl WasmColorPalette {
#[wasm_bindgen(js_name = createViridis)]
pub fn create_viridis() -> Self {
Self {
palette: ColorPalette::viridis(),
}
}
#[wasm_bindgen(js_name = createPlasma)]
pub fn create_plasma() -> Self {
Self {
palette: ColorPalette::plasma(),
}
}
#[wasm_bindgen(js_name = createTerrain)]
pub fn create_terrain() -> Self {
Self {
palette: ColorPalette::terrain(),
}
}
#[wasm_bindgen(js_name = applyToGrayscale)]
pub fn apply_to_grayscale(&self, data: &mut [u8]) -> Result<(), JsValue> {
self.palette
.apply_to_grayscale(data)
.map_err(|e| JsValue::from_str(&e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_color_palette_interpolation() {
let mut palette = ColorPalette::new("test");
palette.add_entry(0.0, Rgb::new(0, 0, 0));
palette.add_entry(1.0, Rgb::new(255, 255, 255));
let mid = palette.interpolate(0.5).expect("Interpolation failed");
assert!(mid.r > 120 && mid.r < 135);
assert!(mid.g > 120 && mid.g < 135);
assert!(mid.b > 120 && mid.b < 135);
}
#[test]
fn test_gradient_generator() {
let gradient_gen = GradientGenerator::new(Rgb::new(0, 0, 0), Rgb::new(255, 255, 255), 11);
let gradient = gradient_gen.generate();
assert_eq!(gradient.len(), 11);
assert_eq!(gradient[0], Rgb::new(0, 0, 0));
assert_eq!(gradient[10], Rgb::new(255, 255, 255));
}
#[test]
fn test_color_correction_identity() {
let matrix = ColorCorrectionMatrix::identity();
let color = Rgb::new(128, 64, 192);
let result = matrix.apply(color);
assert_eq!(result, color);
}
#[test]
fn test_color_correction_brightness() {
let matrix = ColorCorrectionMatrix::brightness(1.5);
let color = Rgb::new(100, 100, 100);
let result = matrix.apply(color);
assert!(result.r > 100);
assert!(result.g > 100);
assert!(result.b > 100);
}
#[test]
fn test_color_temperature() {
let color = Rgb::new(128, 128, 128);
let warm = ColorTemperature::adjust(color, 0.5);
let cool = ColorTemperature::adjust(color, -0.5);
assert!(warm.r > color.r);
assert!(cool.b > color.b);
}
#[test]
fn test_channel_swap() {
let mut data = vec![255, 0, 0, 255];
ChannelOps::swap_channels(&mut data, 0, 2);
assert_eq!(data[0], 0);
assert_eq!(data[2], 255);
}
#[test]
#[ignore]
fn test_color_quantization() {
let mut data = vec![100, 150, 200, 255];
ColorQuantizer::quantize(&mut data, 4).expect("Quantization failed");
assert!(data[0] == 85 || data[0] == 0);
assert!(data[1] == 170 || data[1] == 85);
assert!(data[2] == 170 || data[2] == 255);
}
#[test]
fn test_posterize() {
let mut data = vec![10, 50, 100, 255, 150, 200, 250, 255];
ColorQuantizer::posterize(&mut data, 4);
for i in (0..data.len()).step_by(4).take(2) {
assert_eq!(data[i] % 64, 0);
assert_eq!(data[i + 1] % 64, 0);
assert_eq!(data[i + 2] % 64, 0);
}
}
}