From 780e358bcc8c880cd1926a5a33e52333d8863aa6 Mon Sep 17 00:00:00 2001 From: charlotte Date: Wed, 25 Mar 2026 13:36:57 -0700 Subject: [PATCH 1/3] Add support color mode. --- crates/processing_ffi/src/color.rs | 40 +++-- crates/processing_ffi/src/lib.rs | 69 +++++++-- crates/processing_pyo3/src/color.rs | 157 ++++++++++--------- crates/processing_pyo3/src/graphics.rs | 49 +++++- crates/processing_pyo3/src/lib.rs | 58 +++++++- crates/processing_render/src/color.rs | 182 +++++++++++++++++++++++ crates/processing_render/src/graphics.rs | 1 + crates/processing_render/src/lib.rs | 28 ++++ 8 files changed, 467 insertions(+), 117 deletions(-) create mode 100644 crates/processing_render/src/color.rs diff --git a/crates/processing_ffi/src/color.rs b/crates/processing_ffi/src/color.rs index 088f44d..59d8d78 100644 --- a/crates/processing_ffi/src/color.rs +++ b/crates/processing_ffi/src/color.rs @@ -1,35 +1,33 @@ use bevy::color::{LinearRgba, Srgba}; +use processing::prelude::color::{ColorMode, ColorSpace}; -/// A sRGB (?) color +/// A color with 4 float components and its color space. #[repr(C)] #[derive(Debug, Clone, Copy)] pub struct Color { - pub r: f32, - pub g: f32, - pub b: f32, + pub c1: f32, + pub c2: f32, + pub c3: f32, pub a: f32, + pub space: u8, } -impl From for bevy::color::Color { - fn from(color: Color) -> Self { - bevy::color::Color::srgba(color.r, color.g, color.b, color.a) +impl Color { + pub fn resolve(self, mode: &ColorMode) -> bevy::color::Color { + let c1 = mode.scale(self.c1, 0); + let c2 = mode.scale(self.c2, 1); + let c3 = mode.scale(self.c3, 2); + let ca = mode.scale(self.a, 3); + mode.space.color(c1, c2, c3, ca) } -} - -impl From for Color { - fn from(lin: LinearRgba) -> Self { - let srgb: Srgba = lin.into(); - srgb.into() - } -} -impl From for Color { - fn from(srgb: Srgba) -> Self { + pub fn from_linear(lin: LinearRgba) -> Self { Color { - r: srgb.red, - g: srgb.green, - b: srgb.blue, - a: srgb.alpha, + c1: lin.red, + c2: lin.green, + c3: lin.blue, + a: lin.alpha, + space: ColorSpace::Linear as u8, } } } diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index e63e542..7228a76 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -173,7 +173,9 @@ pub extern "C" fn processing_background_color(graphics_id: u64, color: Color) { error::clear_error(); let graphics_entity = Entity::from_bits(graphics_id); error::check(|| { - graphics_record_command(graphics_entity, DrawCommand::BackgroundColor(color.into())) + let mode = graphics_get_color_mode(graphics_entity)?; + let color = color.resolve(&mode); + graphics_record_command(graphics_entity, DrawCommand::BackgroundColor(color)) }); } @@ -244,17 +246,45 @@ pub extern "C" fn processing_exit(exit_code: u8) { error::check(|| exit(exit_code)); } +/// Set the color mode for a graphics context. +/// +/// SAFETY: +/// - graphics_id is a valid ID returned from graphics_create. +/// - This is called from the same thread as init. +#[unsafe(no_mangle)] +pub extern "C" fn processing_color_mode( + graphics_id: u64, + space: u8, + max1: f32, + max2: f32, + max3: f32, + max_alpha: f32, +) { + error::clear_error(); + let graphics_entity = Entity::from_bits(graphics_id); + error::check(|| { + let space = processing::prelude::color::ColorSpace::from_u8(space) + .ok_or_else(|| processing::prelude::error::ProcessingError::InvalidArgument( + format!("unknown color space: {space}"), + ))?; + let mode = processing::prelude::color::ColorMode::new(space, max1, max2, max3, max_alpha); + graphics_set_color_mode(graphics_entity, mode) + }); +} + /// Set the fill color. /// /// SAFETY: /// - graphics_id is a valid ID returned from graphics_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] -pub extern "C" fn processing_set_fill(graphics_id: u64, r: f32, g: f32, b: f32, a: f32) { +pub extern "C" fn processing_set_fill(graphics_id: u64, color: Color) { error::clear_error(); let graphics_entity = Entity::from_bits(graphics_id); - let color = bevy::color::Color::srgba(r, g, b, a); - error::check(|| graphics_record_command(graphics_entity, DrawCommand::Fill(color))); + error::check(|| { + let mode = graphics_get_color_mode(graphics_entity)?; + graphics_record_command(graphics_entity, DrawCommand::Fill(color.resolve(&mode))) + }); } /// Set the stroke color. @@ -263,11 +293,13 @@ pub extern "C" fn processing_set_fill(graphics_id: u64, r: f32, g: f32, b: f32, /// - graphics_id is a valid ID returned from graphics_create. /// - This is called from the same thread as init. #[unsafe(no_mangle)] -pub extern "C" fn processing_set_stroke_color(graphics_id: u64, r: f32, g: f32, b: f32, a: f32) { +pub extern "C" fn processing_set_stroke_color(graphics_id: u64, color: Color) { error::clear_error(); let graphics_entity = Entity::from_bits(graphics_id); - let color = bevy::color::Color::srgba(r, g, b, a); - error::check(|| graphics_record_command(graphics_entity, DrawCommand::StrokeColor(color))); + error::check(|| { + let mode = graphics_get_color_mode(graphics_entity)?; + graphics_record_command(graphics_entity, DrawCommand::StrokeColor(color.resolve(&mode))) + }); } /// Set the stroke weight. @@ -565,7 +597,7 @@ pub unsafe extern "C" fn processing_image_readback( unsafe { let buffer_slice = std::slice::from_raw_parts_mut(buffer, buffer_len); for (i, color) in colors.iter().enumerate() { - buffer_slice[i] = Color::from(*color); + buffer_slice[i] = Color::from_linear(*color); } } @@ -1154,9 +1186,12 @@ pub extern "C" fn processing_light_create_directional( ) -> u64 { error::clear_error(); let graphics_entity = Entity::from_bits(graphics_id); - error::check(|| light_create_directional(graphics_entity, color.into(), illuminance)) - .map(|e| e.to_bits()) - .unwrap_or(0) + error::check(|| { + let mode = graphics_get_color_mode(graphics_entity)?; + light_create_directional(graphics_entity, color.resolve(&mode), illuminance) + }) + .map(|e| e.to_bits()) + .unwrap_or(0) } #[unsafe(no_mangle)] @@ -1169,9 +1204,12 @@ pub extern "C" fn processing_light_create_point( ) -> u64 { error::clear_error(); let graphics_entity = Entity::from_bits(graphics_id); - error::check(|| light_create_point(graphics_entity, color.into(), intensity, range, radius)) - .map(|e| e.to_bits()) - .unwrap_or(0) + error::check(|| { + let mode = graphics_get_color_mode(graphics_entity)?; + light_create_point(graphics_entity, color.resolve(&mode), intensity, range, radius) + }) + .map(|e| e.to_bits()) + .unwrap_or(0) } #[unsafe(no_mangle)] @@ -1187,9 +1225,10 @@ pub extern "C" fn processing_light_create_spot( error::clear_error(); let graphics_entity = Entity::from_bits(graphics_id); error::check(|| { + let mode = graphics_get_color_mode(graphics_entity)?; light_create_spot( graphics_entity, - color.into(), + color.resolve(&mode), intensity, range, radius, diff --git a/crates/processing_pyo3/src/color.rs b/crates/processing_pyo3/src/color.rs index 3d43cbc..c119dd2 100644 --- a/crates/processing_pyo3/src/color.rs +++ b/crates/processing_pyo3/src/color.rs @@ -4,7 +4,75 @@ use bevy::color::{ }; use pyo3::{exceptions::PyTypeError, prelude::*, types::PyTuple}; -use crate::math::{PyVec4, PyVecIter, hash_f32}; +use crate::math::{PyVec4, PyVecIter, hash_f32, PyVec3}; + +pub use processing::prelude::color::{ColorMode, ColorSpace}; + +pub(crate) fn parse_numeric(space: &ColorSpace, obj: &Bound<'_, PyAny>) -> PyResult { + if let Ok(v) = obj.extract::() { + return Ok(match space { + ColorSpace::Srgb | ColorSpace::Linear => v as f32 / 255.0, + _ => v as f32, + }); + } + if let Ok(v) = obj.extract::() { + return Ok(v as f32); + } + Err(PyTypeError::new_err("expected int or float")) +} + +fn convert_channel(mode: &ColorMode, obj: &Bound<'_, PyAny>, ch: usize) -> PyResult { + let v = parse_numeric(&mode.space, obj)?; + Ok(mode.scale(v, ch)) +} + +// Accepts a varags of color-like arguments and extracts a Color, applying the provided ColorMode. +pub(crate) fn extract_color_with_mode( + args: &Bound<'_, PyTuple>, + mode: &ColorMode, +) -> PyResult { + let space = mode.space; + let native = space.default_maxes(); + match args.len() { + 0 => Err(PyTypeError::new_err("expected at least 1 argument")), + 1 => { + let first = args.get_item(0)?; + if let Ok(c) = first.extract::>() { + return Ok(c.0); + } + if let Ok(s) = first.extract::() { + return parse_hex(&s); + } + if let Ok(v) = first.extract::>() { + return Ok(space.color(v.0.x, v.0.y, v.0.z, v.0.w)); + } + if let Ok(v) = first.extract::>() { + return Ok(space.color(v.0.x, v.0.y, v.0.z, native[3])); + } + let v = convert_channel(mode,&first, 0)?; + Ok(space.gray(v, native[3])) + } + 2 => { + let v = convert_channel(mode,&args.get_item(0)?, 0)?; + let a = convert_channel(mode,&args.get_item(1)?, 3)?; + Ok(space.gray(v, a)) + } + 3 => { + let c1 = convert_channel(mode,&args.get_item(0)?, 0)?; + let c2 = convert_channel(mode,&args.get_item(1)?, 1)?; + let c3 = convert_channel(mode,&args.get_item(2)?, 2)?; + Ok(space.color(c1, c2, c3, native[3])) + } + 4 => { + let c1 = convert_channel(mode,&args.get_item(0)?, 0)?; + let c2 = convert_channel(mode,&args.get_item(1)?, 1)?; + let c3 = convert_channel(mode,&args.get_item(2)?, 2)?; + let ca = convert_channel(mode,&args.get_item(3)?, 3)?; + Ok(space.color(c1, c2, c3, ca)) + } + _ => Err(PyTypeError::new_err("expected 1-4 arguments")), + } +} #[pyclass(name = "Color", from_py_object)] #[derive(Clone, Debug)] @@ -28,20 +96,6 @@ impl From for PyColor { } } -fn extract_component(obj: &Bound<'_, PyAny>) -> PyResult { - if let Ok(v) = obj.extract::() { - return Ok(v as f32 / 255.0); - } - if let Ok(v) = obj.extract::() { - return Ok(v as f32); - } - Err(PyTypeError::new_err("expected int or float")) -} - -fn to_srgba(color: &Color) -> Srgba { - color.to_srgba() -} - fn components(color: &Color) -> [f32; 4] { use bevy::color::ColorToComponents; match *color { @@ -80,42 +134,7 @@ impl PyColor { #[new] #[pyo3(signature = (*args))] pub fn py_new(args: &Bound<'_, PyTuple>) -> PyResult { - match args.len() { - 0 => Err(PyTypeError::new_err("Color requires at least 1 argument")), - 1 => { - let first = args.get_item(0)?; - if let Ok(c) = first.extract::>() { - return Ok(Self(c.0)); - } - if let Ok(s) = first.extract::() { - return Ok(Self(parse_hex(&s)?)); - } - if let Ok(v) = first.extract::>() { - return Ok(Self(Color::srgba(v.0.x, v.0.y, v.0.z, v.0.w))); - } - let v = extract_component(&first)?; - Ok(Self(Color::srgba(v, v, v, 1.0))) - } - 2 => { - let v = extract_component(&args.get_item(0)?)?; - let a = extract_component(&args.get_item(1)?)?; - Ok(Self(Color::srgba(v, v, v, a))) - } - 3 => { - let r = extract_component(&args.get_item(0)?)?; - let g = extract_component(&args.get_item(1)?)?; - let b = extract_component(&args.get_item(2)?)?; - Ok(Self(Color::srgba(r, g, b, 1.0))) - } - 4 => { - let r = extract_component(&args.get_item(0)?)?; - let g = extract_component(&args.get_item(1)?)?; - let b = extract_component(&args.get_item(2)?)?; - let a = extract_component(&args.get_item(3)?)?; - Ok(Self(Color::srgba(r, g, b, a))) - } - _ => Err(PyTypeError::new_err("Color takes 1-4 arguments")), - } + extract_color_with_mode(args, &ColorMode::default()).map(Self) } #[staticmethod] @@ -225,33 +244,33 @@ impl PyColor { #[getter] fn r(&self) -> f32 { - to_srgba(&self.0).red + self.0.to_srgba().red } #[setter] fn set_r(&mut self, val: f32) { - let mut s = to_srgba(&self.0); + let mut s = self.0.to_srgba(); s.red = val; self.0 = Color::Srgba(s); } #[getter] fn g(&self) -> f32 { - to_srgba(&self.0).green + self.0.to_srgba().green } #[setter] fn set_g(&mut self, val: f32) { - let mut s = to_srgba(&self.0); + let mut s = self.0.to_srgba(); s.green = val; self.0 = Color::Srgba(s); } #[getter] fn b(&self) -> f32 { - to_srgba(&self.0).blue + self.0.to_srgba().blue } #[setter] fn set_b(&mut self, val: f32) { - let mut s = to_srgba(&self.0); + let mut s = self.0.to_srgba(); s.blue = val; self.0 = Color::Srgba(s); } @@ -266,7 +285,7 @@ impl PyColor { } fn to_hex(&self) -> String { - to_srgba(&self.0).to_hex() + self.0.to_srgba().to_hex() } fn with_alpha(&self, a: f32) -> Self { @@ -367,12 +386,12 @@ impl PyColor { fn __eq__(&self, other: &Self) -> bool { // Compare in sRGBA so colors in different spaces can be equal - to_srgba(&self.0) == to_srgba(&other.0) + self.0.to_srgba() == other.0.to_srgba() } fn __hash__(&self) -> u64 { // Hash in sRGBA so equal colors hash the same regardless of space - let s = to_srgba(&self.0); + let s = self.0.to_srgba(); let mut hasher = std::collections::hash_map::DefaultHasher::new(); hash_f32(s.red, &mut hasher); hash_f32(s.green, &mut hasher); @@ -423,9 +442,6 @@ impl ColorLike { } } -pub(crate) fn extract_color(args: &Bound<'_, PyTuple>) -> PyResult { - PyColor::py_new(args).map(|c| c.0) -} fn parse_hex(s: &str) -> PyResult { Srgba::hex(s) @@ -440,7 +456,7 @@ mod tests { #[test] fn test_color_from_srgba() { let c = PyColor(Color::srgba(1.0, 0.0, 0.5, 1.0)); - let s = to_srgba(&c.0); + let s = c.0.to_srgba(); assert!((s.red - 1.0).abs() < 1e-6); assert!((s.green - 0.0).abs() < 1e-6); assert!((s.blue - 0.5).abs() < 1e-6); @@ -450,7 +466,7 @@ mod tests { #[test] fn test_hex_roundtrip() { let c = parse_hex("#FF00FF").unwrap(); - let s = to_srgba(&c); + let s = c.to_srgba(); assert!((s.red - 1.0).abs() < 0.01); assert!((s.green - 0.0).abs() < 0.01); assert!((s.blue - 1.0).abs() < 0.01); @@ -460,7 +476,7 @@ mod tests { #[test] fn test_hex_with_alpha() { let c = parse_hex("#FF000080").unwrap(); - let s = to_srgba(&c); + let s = c.to_srgba(); assert!((s.red - 1.0).abs() < 0.01); assert!((s.alpha - 128.0 / 255.0).abs() < 0.01); } @@ -470,7 +486,7 @@ mod tests { let a = PyColor(Color::srgba(0.0, 0.0, 0.0, 1.0)); let b = PyColor(Color::srgba(1.0, 1.0, 1.0, 1.0)); let mid = a.mix(&b, 0.5); - let s = to_srgba(&mid.0); + let s = mid.0.to_srgba(); assert!((s.red - 0.5).abs() < 0.05); assert!((s.green - 0.5).abs() < 0.05); assert!((s.blue - 0.5).abs() < 0.05); @@ -481,8 +497,8 @@ mod tests { let c = PyColor(Color::srgba(0.5, 0.5, 0.5, 1.0)); let lighter = c.lighter(0.1); let darker = c.darker(0.1); - let sl = to_srgba(&lighter.0); - let sd = to_srgba(&darker.0); + let sl = lighter.0.to_srgba(); + let sd = darker.0.to_srgba(); assert!(sl.red > sd.red || sl.green > sd.green || sl.blue > sd.blue); } @@ -513,7 +529,7 @@ mod tests { #[test] fn test_hsla_roundtrip() { let c = PyColor::hsla(0.0, 1.0, 0.5, 1.0); - let s = to_srgba(&c.0); + let s = c.0.to_srgba(); assert!((s.red - 1.0).abs() < 0.01); assert!(s.green < 0.01); assert!(s.blue < 0.01); @@ -553,4 +569,5 @@ mod tests { assert!((list[0] - 0.1).abs() < 1e-6); assert!((list[3] - 0.4).abs() < 1e-6); } + } diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 152e03b..64ca44b 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -10,7 +10,7 @@ use pyo3::{ prelude::*, types::{PyDict, PyTuple}, }; - +use crate::color::{extract_color_with_mode, ColorMode}; use crate::glfw::GlfwContext; use crate::math::{extract_vec2, extract_vec3, extract_vec4}; @@ -213,6 +213,7 @@ impl Graphics { surface, width, height, + }) } @@ -250,6 +251,7 @@ impl Graphics { surface, width, height, + }) } @@ -299,9 +301,14 @@ impl Graphics { } } + #[pyo3(signature = (*args))] + pub fn color(&self, args: &Bound<'_, PyTuple>) -> PyResult { + extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?).map(crate::color::PyColor::from) + } + #[pyo3(signature = (*args))] pub fn background(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let color = crate::color::extract_color(args)?; + let color = extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?)?; graphics_record_command(self.entity, DrawCommand::BackgroundColor(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -313,7 +320,7 @@ impl Graphics { #[pyo3(signature = (*args))] pub fn fill(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let color = crate::color::extract_color(args)?; + let color = extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?)?; graphics_record_command(self.entity, DrawCommand::Fill(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -325,7 +332,7 @@ impl Graphics { #[pyo3(signature = (*args))] pub fn stroke(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let color = crate::color::extract_color(args)?; + let color = extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?)?; graphics_record_command(self.entity, DrawCommand::StrokeColor(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -450,7 +457,7 @@ impl Graphics { #[pyo3(signature = (*args))] pub fn emissive(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let color = crate::color::extract_color(args)?; + let color = extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?)?; graphics_record_command(self.entity, DrawCommand::Emissive(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -492,6 +499,38 @@ impl Graphics { .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } + #[pyo3(name = "color_mode", signature = (mode, max1=None, max2=None, max3=None, max_alpha=None))] + pub fn set_color_mode<'py>( + &self, + mode: u8, + max1: Option<&Bound<'py, PyAny>>, + max2: Option<&Bound<'py, PyAny>>, + max3: Option<&Bound<'py, PyAny>>, + max_alpha: Option<&Bound<'py, PyAny>>, + ) -> PyResult<()> { + let space = crate::color::ColorSpace::from_u8(mode) + .ok_or_else(|| PyRuntimeError::new_err(format!("unknown color space: {mode}")))?; + let parse = |obj: &Bound<'py, PyAny>| crate::color::parse_numeric(&space, obj); + let new_mode = match (max1, max2, max3, max_alpha) { + // color_mode(MODE) + (None, _, _, _) => ColorMode::with_defaults(space), + // color_mode(MODE, max) + (Some(m), None, _, _) => ColorMode::with_uniform_max(space, parse(m)?), + // color_mode(MODE, max1, max2, max3) + (Some(m1), Some(m2), Some(m3), None) => { + let defaults = space.default_maxes(); + ColorMode::new(space, parse(m1)?, parse(m2)?, parse(m3)?, defaults[3]) + } + // color_mode(MODE, max1, max2, max3, maxA) + (Some(m1), Some(m2), Some(m3), Some(ma)) => { + ColorMode::new(space, parse(m1)?, parse(m2)?, parse(m3)?, parse(ma)?) + } + _ => return Err(PyRuntimeError::new_err("expected 1, 2, 4, or 5 arguments")), + }; + graphics_set_color_mode(self.entity, new_mode) + .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) + } + pub fn begin_draw(&self) -> PyResult<()> { graphics_begin_draw(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index b127b9f..18215d1 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -141,6 +141,28 @@ mod mewnala { #[pymodule_export] const BEVEL: u8 = 2; + // color space constants for color_mode() + #[pymodule_export] + const SRGB: u8 = 0; + #[pymodule_export] + const LINEAR: u8 = 1; + #[pymodule_export] + const HSL: u8 = 2; + #[pymodule_export] + const HSV: u8 = 3; + #[pymodule_export] + const HWB: u8 = 4; + #[pymodule_export] + const OKLAB: u8 = 5; + #[pymodule_export] + const OKLCH: u8 = 6; + #[pymodule_export] + const LAB: u8 = 7; + #[pymodule_export] + const LCH: u8 = 8; + #[pymodule_export] + const XYZ: u8 = 9; + #[pymodule] mod math { use super::*; @@ -188,12 +210,6 @@ mod mewnala { #[pymodule_export] use crate::color::PyColor; - #[pyfunction] - #[pyo3(signature = (*args))] - fn color(args: &Bound<'_, PyTuple>) -> PyResult { - PyColor::py_new(args) - } - #[pyfunction] fn hex(s: &str) -> PyResult { PyColor::hex(s) @@ -514,6 +530,21 @@ mod mewnala { graphics!(module).draw_geometry(&*geometry.extract::>()?) } + #[pyfunction(name = "color")] + #[pyo3(pass_module, signature = (*args))] + fn create_color( + module: &Bound<'_, PyModule>, + args: &Bound<'_, PyTuple>, + ) -> PyResult { + match get_graphics(module)? { + Some(g) => g.color(args), + None => { + let mode = super::color::ColorMode::default(); + super::color::extract_color_with_mode(args, &mode).map(super::color::PyColor::from) + } + } + } + #[pyfunction] #[pyo3(pass_module, signature = (*args))] fn background(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { @@ -526,6 +557,21 @@ mod mewnala { } } + #[pyfunction] + #[pyo3(pass_module, signature = (mode, max1=None, max2=None, max3=None, max_alpha=None))] + fn color_mode<'py>( + module: &Bound<'py, PyModule>, + mode: u8, + max1: Option<&Bound<'py, PyAny>>, + max2: Option<&Bound<'py, PyAny>>, + max3: Option<&Bound<'py, PyAny>>, + max_alpha: Option<&Bound<'py, PyAny>>, + ) -> PyResult<()> { + let graphics = get_graphics(module)? + .ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; + graphics.set_color_mode(mode, max1, max2, max3, max_alpha) + } + #[pyfunction] #[pyo3(pass_module, signature = (*args))] fn fill(module: &Bound<'_, PyModule>, args: &Bound<'_, PyTuple>) -> PyResult<()> { diff --git a/crates/processing_render/src/color.rs b/crates/processing_render/src/color.rs new file mode 100644 index 0000000..2bb83b6 --- /dev/null +++ b/crates/processing_render/src/color.rs @@ -0,0 +1,182 @@ +use bevy::color::{ + Color, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Oklaba, Oklcha, Srgba, Xyza, +}; +use bevy::prelude::Component; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[repr(u8)] +pub enum ColorSpace { + Srgb = 0, + Linear = 1, + Hsl = 2, + Hsv = 3, + Hwb = 4, + Oklab = 5, + Oklch = 6, + Lab = 7, + Lch = 8, + Xyz = 9, +} + +impl ColorSpace { + pub fn from_u8(v: u8) -> Option { + match v { + 0 => Some(Self::Srgb), + 1 => Some(Self::Linear), + 2 => Some(Self::Hsl), + 3 => Some(Self::Hsv), + 4 => Some(Self::Hwb), + 5 => Some(Self::Oklab), + 6 => Some(Self::Oklch), + 7 => Some(Self::Lab), + 8 => Some(Self::Lch), + 9 => Some(Self::Xyz), + _ => None, + } + } + + pub fn default_maxes(&self) -> [f32; 4] { + match self { + Self::Srgb | Self::Linear | Self::Oklab | Self::Xyz => [1.0, 1.0, 1.0, 1.0], + Self::Hsl | Self::Hsv | Self::Hwb => [360.0, 1.0, 1.0, 1.0], + Self::Oklch => [1.0, 1.0, 360.0, 1.0], + Self::Lab => [100.0, 1.0, 1.0, 1.0], + Self::Lch => [100.0, 1.0, 360.0, 1.0], + } + } + + pub fn color(self, c1: f32, c2: f32, c3: f32, alpha: f32) -> Color { + match self { + Self::Srgb => Color::Srgba(Srgba::new(c1, c2, c3, alpha)), + Self::Linear => Color::LinearRgba(LinearRgba::new(c1, c2, c3, alpha)), + Self::Hsl => Color::Hsla(Hsla::new(c1, c2, c3, alpha)), + Self::Hsv => Color::Hsva(Hsva::new(c1, c2, c3, alpha)), + Self::Hwb => Color::Hwba(Hwba::new(c1, c2, c3, alpha)), + Self::Oklab => Color::Oklaba(Oklaba::new(c1, c2, c3, alpha)), + Self::Oklch => Color::Oklcha(Oklcha::new(c1, c2, c3, alpha)), + Self::Lab => Color::Laba(Laba::new(c1, c2, c3, alpha)), + Self::Lch => Color::Lcha(Lcha::new(c1, c2, c3, alpha)), + Self::Xyz => Color::Xyza(Xyza::new(c1, c2, c3, alpha)), + } + } + + pub fn gray(self, v: f32, alpha: f32) -> Color { + match self { + Self::Srgb | Self::Linear | Self::Xyz => self.color(v, v, v, alpha), + Self::Hsl | Self::Hsv | Self::Hwb => self.color(0.0, 0.0, v, alpha), + Self::Oklab | Self::Lab => self.color(v, 0.0, 0.0, alpha), + Self::Oklch | Self::Lch => self.color(v, 0.0, 0.0, alpha), + } + } +} + +#[derive(Debug, Clone, Copy, Component)] +pub struct ColorMode { + pub space: ColorSpace, + pub max: [f32; 4], +} + +impl Default for ColorMode { + fn default() -> Self { + Self::with_defaults(ColorSpace::Srgb) + } +} + +impl ColorMode { + pub fn new(space: ColorSpace, max1: f32, max2: f32, max3: f32, max_alpha: f32) -> Self { + Self { + space, + max: [max1, max2, max3, max_alpha], + } + } + + pub fn with_defaults(space: ColorSpace) -> Self { + Self { + space, + max: space.default_maxes(), + } + } + + pub fn with_uniform_max(space: ColorSpace, max: f32) -> Self { + Self { + space, + max: [max, max, max, max], + } + } + + /// Scale a raw float value for a given channel to the 0-1 normalized range. + pub fn scale(&self, value: f32, ch: usize) -> f32 { + let native = self.space.default_maxes(); + value / self.max[ch] * native[ch] + } + +} + +#[cfg(test)] +mod tests { + use super::*; + use bevy::color::ColorToComponents; + + #[test] + fn test_srgb_color() { + let c = ColorSpace::Srgb.color(1.0, 0.0, 0.0, 1.0); + let s: Srgba = c.into(); + assert!((s.red - 1.0).abs() < 1e-6); + assert!(s.green.abs() < 1e-6); + } + + #[test] + fn test_hsl_color() { + let c = ColorSpace::Hsl.color(180.0, 0.5, 0.5, 1.0); + let h: Hsla = c.into(); + assert!((h.hue - 180.0).abs() < 0.5); + assert!((h.saturation - 0.5).abs() < 0.01); + assert!((h.lightness - 0.5).abs() < 0.01); + } + + #[test] + fn test_hsv_red() { + let c = ColorSpace::Hsv.color(0.0, 1.0, 1.0, 1.0); + let s: Srgba = c.into(); + assert!((s.red - 1.0).abs() < 0.01); + assert!(s.green < 0.01); + } + + #[test] + fn test_gray_srgb() { + let c = ColorSpace::Srgb.gray(0.5, 1.0); + let s: Srgba = c.into(); + assert!((s.red - 0.5).abs() < 0.01); + assert!((s.green - 0.5).abs() < 0.01); + assert!((s.blue - 0.5).abs() < 0.01); + } + + #[test] + fn test_gray_hsl() { + let c = ColorSpace::Hsl.gray(0.5, 1.0); + let s: Srgba = c.into(); + assert!((s.red - 0.5).abs() < 0.05); + assert!((s.green - 0.5).abs() < 0.05); + } + + #[test] + fn test_scale_identity() { + let mode = ColorMode::default(); + assert!((mode.scale(0.5, 0) - 0.5).abs() < 1e-6); + } + + #[test] + fn test_scale_255() { + let mode = ColorMode::with_uniform_max(ColorSpace::Srgb, 255.0); + assert!((mode.scale(255.0, 0) - 1.0).abs() < 1e-4); + } + + + #[test] + fn test_scale_hsl_percent() { + let mode = ColorMode::new(ColorSpace::Hsl, 360.0, 100.0, 100.0, 1.0); + assert!((mode.scale(180.0, 0) - 180.0).abs() < 0.01); + assert!((mode.scale(50.0, 1) - 0.5).abs() < 1e-4); + assert!((mode.scale(50.0, 2) - 0.5).abs() < 1e-4); + } +} diff --git a/crates/processing_render/src/graphics.rs b/crates/processing_render/src/graphics.rs index 48567b1..9d101c8 100644 --- a/crates/processing_render/src/graphics.rs +++ b/crates/processing_render/src/graphics.rs @@ -239,6 +239,7 @@ pub fn create( render_layer, CommandBuffer::new(), RenderState::default(), + crate::color::ColorMode::default(), SurfaceSize(width, height), Graphics { readback_buffer, diff --git a/crates/processing_render/src/lib.rs b/crates/processing_render/src/lib.rs index 5d68675..2eb6954 100644 --- a/crates/processing_render/src/lib.rs +++ b/crates/processing_render/src/lib.rs @@ -1,5 +1,6 @@ #![allow(clippy::module_inception)] +pub mod color; pub mod geometry; pub mod gltf; mod graphics; @@ -371,6 +372,33 @@ pub fn graphics_update_region( }) } +/// Set the color mode for a graphics entity. +pub fn graphics_set_color_mode( + graphics_entity: Entity, + mode: color::ColorMode, +) -> error::Result<()> { + app_mut(|app| { + let mut entity = app + .world_mut() + .get_entity_mut(graphics_entity) + .map_err(|_| error::ProcessingError::GraphicsNotFound)?; + if let Some(mut cm) = entity.get_mut::() { + *cm = mode; + } + Ok(()) + }) +} + +/// Get the color mode for a graphics entity. +pub fn graphics_get_color_mode(graphics_entity: Entity) -> error::Result { + app_mut(|app| { + app.world() + .get::(graphics_entity) + .copied() + .ok_or(error::ProcessingError::GraphicsNotFound) + }) +} + /// Record a drawing command for a window pub fn graphics_record_command(graphics_entity: Entity, cmd: DrawCommand) -> error::Result<()> { app_mut(|app| { From 89e65a35d68286d37829a989db1eba1873f4a678 Mon Sep 17 00:00:00 2001 From: charlotte Date: Wed, 25 Mar 2026 13:54:06 -0700 Subject: [PATCH 2/3] Fixup ints. --- crates/processing_pyo3/src/color.rs | 24 ++++++++++++++++++------ crates/processing_pyo3/src/graphics.rs | 8 ++++---- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/crates/processing_pyo3/src/color.rs b/crates/processing_pyo3/src/color.rs index c119dd2..09fa6d8 100644 --- a/crates/processing_pyo3/src/color.rs +++ b/crates/processing_pyo3/src/color.rs @@ -8,12 +8,24 @@ use crate::math::{PyVec4, PyVecIter, hash_f32, PyVec3}; pub use processing::prelude::color::{ColorMode, ColorSpace}; -pub(crate) fn parse_numeric(space: &ColorSpace, obj: &Bound<'_, PyAny>) -> PyResult { +fn int_maxes(space: &ColorSpace) -> [f32; 4] { + match space { + ColorSpace::Srgb | ColorSpace::Linear => [255.0, 255.0, 255.0, 255.0], + ColorSpace::Hsl | ColorSpace::Hsv | ColorSpace::Hwb => [360.0, 100.0, 100.0, 255.0], + ColorSpace::Oklch => [100.0, 100.0, 360.0, 255.0], + ColorSpace::Oklab => [100.0, 100.0, 100.0, 255.0], + ColorSpace::Lab => [100.0, 100.0, 100.0, 255.0], + ColorSpace::Lch => [100.0, 100.0, 360.0, 255.0], + ColorSpace::Xyz => [100.0, 100.0, 100.0, 255.0], + } +} + +/// Parse a Python int or float into an f32 for a given channel. +pub(crate) fn parse_numeric(space: &ColorSpace, obj: &Bound<'_, PyAny>, ch: usize) -> PyResult { if let Ok(v) = obj.extract::() { - return Ok(match space { - ColorSpace::Srgb | ColorSpace::Linear => v as f32 / 255.0, - _ => v as f32, - }); + let native = space.default_maxes(); + let imax = int_maxes(space); + return Ok(v as f32 / imax[ch] * native[ch]); } if let Ok(v) = obj.extract::() { return Ok(v as f32); @@ -22,7 +34,7 @@ pub(crate) fn parse_numeric(space: &ColorSpace, obj: &Bound<'_, PyAny>) -> PyRes } fn convert_channel(mode: &ColorMode, obj: &Bound<'_, PyAny>, ch: usize) -> PyResult { - let v = parse_numeric(&mode.space, obj)?; + let v = parse_numeric(&mode.space, obj, ch)?; Ok(mode.scale(v, ch)) } diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index 64ca44b..d70352f 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -510,20 +510,20 @@ impl Graphics { ) -> PyResult<()> { let space = crate::color::ColorSpace::from_u8(mode) .ok_or_else(|| PyRuntimeError::new_err(format!("unknown color space: {mode}")))?; - let parse = |obj: &Bound<'py, PyAny>| crate::color::parse_numeric(&space, obj); + let parse = |obj: &Bound<'py, PyAny>, ch: usize| crate::color::parse_numeric(&space, obj, ch); let new_mode = match (max1, max2, max3, max_alpha) { // color_mode(MODE) (None, _, _, _) => ColorMode::with_defaults(space), // color_mode(MODE, max) - (Some(m), None, _, _) => ColorMode::with_uniform_max(space, parse(m)?), + (Some(m), None, _, _) => ColorMode::with_uniform_max(space, parse(m, 0)?), // color_mode(MODE, max1, max2, max3) (Some(m1), Some(m2), Some(m3), None) => { let defaults = space.default_maxes(); - ColorMode::new(space, parse(m1)?, parse(m2)?, parse(m3)?, defaults[3]) + ColorMode::new(space, parse(m1, 0)?, parse(m2, 1)?, parse(m3, 2)?, defaults[3]) } // color_mode(MODE, max1, max2, max3, maxA) (Some(m1), Some(m2), Some(m3), Some(ma)) => { - ColorMode::new(space, parse(m1)?, parse(m2)?, parse(m3)?, parse(ma)?) + ColorMode::new(space, parse(m1, 0)?, parse(m2, 1)?, parse(m3, 2)?, parse(ma, 3)?) } _ => return Err(PyRuntimeError::new_err("expected 1, 2, 4, or 5 arguments")), }; From 50c47c284ba2079231eade8a1fc9bf86413f5eac Mon Sep 17 00:00:00 2001 From: charlotte Date: Wed, 25 Mar 2026 14:05:13 -0700 Subject: [PATCH 3/3] Fmt. --- crates/processing_ffi/src/lib.rs | 22 +++++++--- crates/processing_pyo3/src/color.rs | 30 +++++++------ crates/processing_pyo3/src/graphics.rs | 60 +++++++++++++++++++------- crates/processing_pyo3/src/lib.rs | 4 +- crates/processing_render/src/color.rs | 6 +-- 5 files changed, 80 insertions(+), 42 deletions(-) diff --git a/crates/processing_ffi/src/lib.rs b/crates/processing_ffi/src/lib.rs index 7228a76..bbd4784 100644 --- a/crates/processing_ffi/src/lib.rs +++ b/crates/processing_ffi/src/lib.rs @@ -263,10 +263,11 @@ pub extern "C" fn processing_color_mode( error::clear_error(); let graphics_entity = Entity::from_bits(graphics_id); error::check(|| { - let space = processing::prelude::color::ColorSpace::from_u8(space) - .ok_or_else(|| processing::prelude::error::ProcessingError::InvalidArgument( - format!("unknown color space: {space}"), - ))?; + let space = processing::prelude::color::ColorSpace::from_u8(space).ok_or_else(|| { + processing::prelude::error::ProcessingError::InvalidArgument(format!( + "unknown color space: {space}" + )) + })?; let mode = processing::prelude::color::ColorMode::new(space, max1, max2, max3, max_alpha); graphics_set_color_mode(graphics_entity, mode) }); @@ -298,7 +299,10 @@ pub extern "C" fn processing_set_stroke_color(graphics_id: u64, color: Color) { let graphics_entity = Entity::from_bits(graphics_id); error::check(|| { let mode = graphics_get_color_mode(graphics_entity)?; - graphics_record_command(graphics_entity, DrawCommand::StrokeColor(color.resolve(&mode))) + graphics_record_command( + graphics_entity, + DrawCommand::StrokeColor(color.resolve(&mode)), + ) }); } @@ -1206,7 +1210,13 @@ pub extern "C" fn processing_light_create_point( let graphics_entity = Entity::from_bits(graphics_id); error::check(|| { let mode = graphics_get_color_mode(graphics_entity)?; - light_create_point(graphics_entity, color.resolve(&mode), intensity, range, radius) + light_create_point( + graphics_entity, + color.resolve(&mode), + intensity, + range, + radius, + ) }) .map(|e| e.to_bits()) .unwrap_or(0) diff --git a/crates/processing_pyo3/src/color.rs b/crates/processing_pyo3/src/color.rs index 09fa6d8..6427c0c 100644 --- a/crates/processing_pyo3/src/color.rs +++ b/crates/processing_pyo3/src/color.rs @@ -4,7 +4,7 @@ use bevy::color::{ }; use pyo3::{exceptions::PyTypeError, prelude::*, types::PyTuple}; -use crate::math::{PyVec4, PyVecIter, hash_f32, PyVec3}; +use crate::math::{PyVec3, PyVec4, PyVecIter, hash_f32}; pub use processing::prelude::color::{ColorMode, ColorSpace}; @@ -21,7 +21,11 @@ fn int_maxes(space: &ColorSpace) -> [f32; 4] { } /// Parse a Python int or float into an f32 for a given channel. -pub(crate) fn parse_numeric(space: &ColorSpace, obj: &Bound<'_, PyAny>, ch: usize) -> PyResult { +pub(crate) fn parse_numeric( + space: &ColorSpace, + obj: &Bound<'_, PyAny>, + ch: usize, +) -> PyResult { if let Ok(v) = obj.extract::() { let native = space.default_maxes(); let imax = int_maxes(space); @@ -61,25 +65,25 @@ pub(crate) fn extract_color_with_mode( if let Ok(v) = first.extract::>() { return Ok(space.color(v.0.x, v.0.y, v.0.z, native[3])); } - let v = convert_channel(mode,&first, 0)?; + let v = convert_channel(mode, &first, 0)?; Ok(space.gray(v, native[3])) } 2 => { - let v = convert_channel(mode,&args.get_item(0)?, 0)?; - let a = convert_channel(mode,&args.get_item(1)?, 3)?; + let v = convert_channel(mode, &args.get_item(0)?, 0)?; + let a = convert_channel(mode, &args.get_item(1)?, 3)?; Ok(space.gray(v, a)) } 3 => { - let c1 = convert_channel(mode,&args.get_item(0)?, 0)?; - let c2 = convert_channel(mode,&args.get_item(1)?, 1)?; - let c3 = convert_channel(mode,&args.get_item(2)?, 2)?; + let c1 = convert_channel(mode, &args.get_item(0)?, 0)?; + let c2 = convert_channel(mode, &args.get_item(1)?, 1)?; + let c3 = convert_channel(mode, &args.get_item(2)?, 2)?; Ok(space.color(c1, c2, c3, native[3])) } 4 => { - let c1 = convert_channel(mode,&args.get_item(0)?, 0)?; - let c2 = convert_channel(mode,&args.get_item(1)?, 1)?; - let c3 = convert_channel(mode,&args.get_item(2)?, 2)?; - let ca = convert_channel(mode,&args.get_item(3)?, 3)?; + let c1 = convert_channel(mode, &args.get_item(0)?, 0)?; + let c2 = convert_channel(mode, &args.get_item(1)?, 1)?; + let c3 = convert_channel(mode, &args.get_item(2)?, 2)?; + let ca = convert_channel(mode, &args.get_item(3)?, 3)?; Ok(space.color(c1, c2, c3, ca)) } _ => Err(PyTypeError::new_err("expected 1-4 arguments")), @@ -454,7 +458,6 @@ impl ColorLike { } } - fn parse_hex(s: &str) -> PyResult { Srgba::hex(s) .map(|srgba| Color::Srgba(srgba)) @@ -581,5 +584,4 @@ mod tests { assert!((list[0] - 0.1).abs() < 1e-6); assert!((list[3] - 0.4).abs() < 1e-6); } - } diff --git a/crates/processing_pyo3/src/graphics.rs b/crates/processing_pyo3/src/graphics.rs index d70352f..8284050 100644 --- a/crates/processing_pyo3/src/graphics.rs +++ b/crates/processing_pyo3/src/graphics.rs @@ -1,3 +1,6 @@ +use crate::color::{ColorMode, extract_color_with_mode}; +use crate::glfw::GlfwContext; +use crate::math::{extract_vec2, extract_vec3, extract_vec4}; use bevy::{ color::{ColorToPacked, Srgba}, math::Vec4, @@ -10,9 +13,6 @@ use pyo3::{ prelude::*, types::{PyDict, PyTuple}, }; -use crate::color::{extract_color_with_mode, ColorMode}; -use crate::glfw::GlfwContext; -use crate::math::{extract_vec2, extract_vec3, extract_vec4}; #[pyclass(unsendable)] pub struct Surface { @@ -213,7 +213,6 @@ impl Graphics { surface, width, height, - }) } @@ -251,7 +250,6 @@ impl Graphics { surface, width, height, - }) } @@ -303,12 +301,21 @@ impl Graphics { #[pyo3(signature = (*args))] pub fn color(&self, args: &Bound<'_, PyTuple>) -> PyResult { - extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?).map(crate::color::PyColor::from) + extract_color_with_mode( + args, + &graphics_get_color_mode(self.entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?, + ) + .map(crate::color::PyColor::from) } #[pyo3(signature = (*args))] pub fn background(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let color = extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?)?; + let color = extract_color_with_mode( + args, + &graphics_get_color_mode(self.entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?, + )?; graphics_record_command(self.entity, DrawCommand::BackgroundColor(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -320,7 +327,11 @@ impl Graphics { #[pyo3(signature = (*args))] pub fn fill(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let color = extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?)?; + let color = extract_color_with_mode( + args, + &graphics_get_color_mode(self.entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?, + )?; graphics_record_command(self.entity, DrawCommand::Fill(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -332,7 +343,11 @@ impl Graphics { #[pyo3(signature = (*args))] pub fn stroke(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let color = extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?)?; + let color = extract_color_with_mode( + args, + &graphics_get_color_mode(self.entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?, + )?; graphics_record_command(self.entity, DrawCommand::StrokeColor(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -457,7 +472,11 @@ impl Graphics { #[pyo3(signature = (*args))] pub fn emissive(&self, args: &Bound<'_, PyTuple>) -> PyResult<()> { - let color = extract_color_with_mode(args, &graphics_get_color_mode(self.entity).map_err(|e| PyRuntimeError::new_err(format!("{e}")))?)?; + let color = extract_color_with_mode( + args, + &graphics_get_color_mode(self.entity) + .map_err(|e| PyRuntimeError::new_err(format!("{e}")))?, + )?; graphics_record_command(self.entity, DrawCommand::Emissive(color)) .map_err(|e| PyRuntimeError::new_err(format!("{e}"))) } @@ -510,7 +529,8 @@ impl Graphics { ) -> PyResult<()> { let space = crate::color::ColorSpace::from_u8(mode) .ok_or_else(|| PyRuntimeError::new_err(format!("unknown color space: {mode}")))?; - let parse = |obj: &Bound<'py, PyAny>, ch: usize| crate::color::parse_numeric(&space, obj, ch); + let parse = + |obj: &Bound<'py, PyAny>, ch: usize| crate::color::parse_numeric(&space, obj, ch); let new_mode = match (max1, max2, max3, max_alpha) { // color_mode(MODE) (None, _, _, _) => ColorMode::with_defaults(space), @@ -519,12 +539,22 @@ impl Graphics { // color_mode(MODE, max1, max2, max3) (Some(m1), Some(m2), Some(m3), None) => { let defaults = space.default_maxes(); - ColorMode::new(space, parse(m1, 0)?, parse(m2, 1)?, parse(m3, 2)?, defaults[3]) + ColorMode::new( + space, + parse(m1, 0)?, + parse(m2, 1)?, + parse(m3, 2)?, + defaults[3], + ) } // color_mode(MODE, max1, max2, max3, maxA) - (Some(m1), Some(m2), Some(m3), Some(ma)) => { - ColorMode::new(space, parse(m1, 0)?, parse(m2, 1)?, parse(m3, 2)?, parse(ma, 3)?) - } + (Some(m1), Some(m2), Some(m3), Some(ma)) => ColorMode::new( + space, + parse(m1, 0)?, + parse(m2, 1)?, + parse(m3, 2)?, + parse(ma, 3)?, + ), _ => return Err(PyRuntimeError::new_err("expected 1, 2, 4, or 5 arguments")), }; graphics_set_color_mode(self.entity, new_mode) diff --git a/crates/processing_pyo3/src/lib.rs b/crates/processing_pyo3/src/lib.rs index 18215d1..8a4d428 100644 --- a/crates/processing_pyo3/src/lib.rs +++ b/crates/processing_pyo3/src/lib.rs @@ -567,8 +567,8 @@ mod mewnala { max3: Option<&Bound<'py, PyAny>>, max_alpha: Option<&Bound<'py, PyAny>>, ) -> PyResult<()> { - let graphics = get_graphics(module)? - .ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; + let graphics = + get_graphics(module)?.ok_or_else(|| PyRuntimeError::new_err("call size() first"))?; graphics.set_color_mode(mode, max1, max2, max3, max_alpha) } diff --git a/crates/processing_render/src/color.rs b/crates/processing_render/src/color.rs index 2bb83b6..d912c78 100644 --- a/crates/processing_render/src/color.rs +++ b/crates/processing_render/src/color.rs @@ -1,6 +1,4 @@ -use bevy::color::{ - Color, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Oklaba, Oklcha, Srgba, Xyza, -}; +use bevy::color::{Color, Hsla, Hsva, Hwba, Laba, Lcha, LinearRgba, Oklaba, Oklcha, Srgba, Xyza}; use bevy::prelude::Component; #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -109,7 +107,6 @@ impl ColorMode { let native = self.space.default_maxes(); value / self.max[ch] * native[ch] } - } #[cfg(test)] @@ -171,7 +168,6 @@ mod tests { assert!((mode.scale(255.0, 0) - 1.0).abs() < 1e-4); } - #[test] fn test_scale_hsl_percent() { let mode = ColorMode::new(ColorSpace::Hsl, 360.0, 100.0, 100.0, 1.0);