From 0d2e80145cb4fbc8ae2ddcb7dc2898116660755d Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Sat, 1 Aug 2026 15:52:44 -0400 Subject: [PATCH] Add color blending --- enkou-shaders/src/image.rs | 79 ++++++++++++++++++++++++++++++++++++++ enkou-shaders/src/lib.rs | 1 + 2 files changed, 80 insertions(+) create mode 100644 enkou-shaders/src/image.rs diff --git a/enkou-shaders/src/image.rs b/enkou-shaders/src/image.rs new file mode 100644 index 0000000..62b49f9 --- /dev/null +++ b/enkou-shaders/src/image.rs @@ -0,0 +1,79 @@ +//! Image +use glam::Vec4; +use libm::floorf; + +/// Blending modes for mapping IFS color values (which are on a scale `[0, 1]`) +/// to RGBA colors. +#[derive(Copy, Clone, Default)] +#[repr(u32)] +pub enum BlendMode { + /// Map IFS color values to a linear blend of the nearest two palette colors + #[default] + Linear = 0, + + /// Map IFS color values to the nearest single palette color + Step = 1, +} + +impl BlendMode { + /// Map an IFS color value to RGBA color from the provided palette. + pub fn ifs_to_rgb(&self, color: f32, palette: &[Vec4]) -> Vec4 { + let colors_m_one = palette.len() - 1; + let period = 1.0 / colors_m_one as f32; + let index_lower = floorf(color / period) as usize; + let index_upper = (index_lower + 1).clamp(0, colors_m_one); + let rem = color % period / period; + + match self { + BlendMode::Linear => palette[index_lower].lerp(palette[index_upper], rem), + BlendMode::Step => palette[index_lower], + } + } +} + +// UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value +unsafe impl bytemuck::Zeroable for BlendMode {} +// UNSAFE: Sound because enum has guaranteed layout (u32) and defined zero-value +unsafe impl bytemuck::Pod for BlendMode {} + +#[cfg(test)] +mod test { + use glam::Vec4; + use crate::image::BlendMode; + + #[test] + fn blend_linear() { + let ifs_to_rgb = |color, palette| BlendMode::Linear.ifs_to_rgb(color, palette); + + let palette = &[Vec4::splat(0.0), Vec4::splat(1.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), Vec4::splat(0.0)); + assert_eq!(ifs_to_rgb(0.5, palette), Vec4::splat(0.5)); + assert_eq!(ifs_to_rgb(1.0, palette), Vec4::splat(1.0)); + + let palette = &[Vec4::splat(1.0), Vec4::splat(2.0), Vec4::splat(3.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), Vec4::splat(1.0)); + assert_eq!(ifs_to_rgb(0.5, palette), Vec4::splat(2.0)); + assert_eq!(ifs_to_rgb(1.0, palette), Vec4::splat(3.0)); + + let palette = &[Vec4::splat(1.0), Vec4::splat(2.0), Vec4::splat(3.0), Vec4::splat(4.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), Vec4::splat(1.0)); + assert_eq!(ifs_to_rgb(0.5, palette), Vec4::splat(2.5)); + assert_eq!(ifs_to_rgb(1.0, palette), Vec4::splat(4.0)); + } + + #[test] + fn blend_step() { + let ifs_to_rgb = |color, palette| BlendMode::Step.ifs_to_rgb(color, palette); + + let palette = &[Vec4::splat(0.0), Vec4::splat(1.0)]; + assert_eq!(ifs_to_rgb(0.5, palette), Vec4::splat(0.0)); + + let palette = &[Vec4::splat(1.0), Vec4::splat(2.0), Vec4::splat(3.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), palette[0]); + assert_eq!(ifs_to_rgb(0.25, palette), palette[0]); + assert_eq!(ifs_to_rgb(0.4, palette), palette[0]); + assert_eq!(ifs_to_rgb(0.5, palette), palette[1]); + assert_eq!(ifs_to_rgb(0.7, palette), palette[1]); + assert_eq!(ifs_to_rgb(1.0, palette), palette[2]); + } +} diff --git a/enkou-shaders/src/lib.rs b/enkou-shaders/src/lib.rs index 0e485ae..38d0cba 100644 --- a/enkou-shaders/src/lib.rs +++ b/enkou-shaders/src/lib.rs @@ -6,6 +6,7 @@ pub mod camera; pub mod chaos_game; pub mod transform; pub mod variation; +mod image; use glam::Affine2;