From 0d9d2b693ee247a64eb8cf2085fa6c124278ece3 Mon Sep 17 00:00:00 2001 From: Bradlee Speice Date: Fri, 3 Jul 2026 16:18:01 -0400 Subject: [PATCH] Fix palette blend modes, add unit tests --- enkou-shaders/src/camera.rs | 68 +++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 14 deletions(-) diff --git a/enkou-shaders/src/camera.rs b/enkou-shaders/src/camera.rs index 82a752f..93adbee 100644 --- a/enkou-shaders/src/camera.rs +++ b/enkou-shaders/src/camera.rs @@ -3,7 +3,7 @@ //! Map points from the IFS coordinate system to pixel coordinates. This is a lossy transformation. use bytemuck::{Pod, Zeroable}; use glam::{Affine2, IVec2, UVec2, Vec2, Vec4, Vec4Swizzles, vec2}; -use libm::{ceilf, floorf, powf}; +use libm::{floorf, powf}; /// Blending modes for mapping IFS color values (which are on a scale `[0, 1]`) /// to RGBA colors. @@ -26,15 +26,15 @@ impl Default for BlendMode { 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 palette_index = color * palette.len() as f32; - let palette_index_lower = floorf(palette_index) as usize; - let palette_index_upper = ceilf(palette_index) as usize; + 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[palette_index_lower] + palette[palette_index_upper]) / 2.0 - } - BlendMode::Step => palette[palette_index_lower], + BlendMode::Linear => palette[index_lower].lerp(palette[index_upper], rem), + BlendMode::Step => palette[index_lower], } } } @@ -161,12 +161,52 @@ pub mod entry { #[cfg(test)] mod test { - use crate::camera::Camera; - use glam::{Affine2, Vec2, ivec2, uvec2, vec2}; + use crate::camera::{BlendMode, Camera}; + use glam::{Affine2, Vec2, Vec4, ivec2, uvec2, vec2}; use libm::powf; + fn vec4s(value: f32) -> Vec4 { + Vec4::splat(value) + } + #[test] - fn manual_camera() { + fn blend_linear() { + let ifs_to_rgb = |color, palette| BlendMode::Linear.ifs_to_rgb(color, palette); + + let palette = &[vec4s(0.0), vec4s(1.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), vec4s(0.0)); + assert_eq!(ifs_to_rgb(0.5, palette), vec4s(0.5)); + assert_eq!(ifs_to_rgb(1.0, palette), vec4s(1.0)); + + let palette = &[vec4s(1.0), vec4s(2.0), vec4s(3.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), vec4s(1.0)); + assert_eq!(ifs_to_rgb(0.5, palette), vec4s(2.0)); + assert_eq!(ifs_to_rgb(1.0, palette), vec4s(3.0)); + + let palette = &[vec4s(1.0), vec4s(2.0), vec4s(3.0), vec4s(4.0)]; + assert_eq!(ifs_to_rgb(0.0, palette), vec4s(1.0)); + assert_eq!(ifs_to_rgb(0.5, palette), vec4s(2.5)); + assert_eq!(ifs_to_rgb(1.0, palette), vec4s(4.0)); + } + + #[test] + fn blend_step() { + let ifs_to_rgb = |color, palette| BlendMode::Step.ifs_to_rgb(color, palette); + + let palette = &[vec4s(0.0), vec4s(1.0)]; + assert_eq!(ifs_to_rgb(0.5, palette), vec4s(0.0)); + + let palette = &[vec4s(1.0), vec4s(2.0), vec4s(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]); + } + + #[test] + fn camera_manual() { let starting_point = vec2(1.0, 1.0); // Move the origin; points move right and up by one unit, giving us (2.0, 2.0) @@ -204,7 +244,7 @@ mod test { } #[test] - fn point_outside_camera() { + fn camera_point_outside_image() { // Scale 250 for an image 1000 x 1000 gives an effective range of [-2, 2] let camera = Camera::new( uvec2(1000, 1000), @@ -219,7 +259,7 @@ mod test { } #[test] - fn point_outside_camera_negative() { + fn camera_point_outside_image_negative() { // Scale 250 for an image 1000 x 1000 gives an effective range of [-2, 2] let camera = Camera::new( uvec2(1000, 1000), @@ -234,7 +274,7 @@ mod test { } #[test] - fn aspect_ratio() { + fn camera_aspect_ratio() { // Scale 100 for an image 1600 x 900 gives an effective X range of [-8, 8], // and effective Y range of [-4.5, 4.5] let camera = Camera::new(