Add color blending

This commit is contained in:
2026-07-12 21:25:31 -04:00
parent fd49ae7256
commit 81a1a2a254
6 changed files with 88 additions and 14 deletions
+4 -4
View File
@@ -116,7 +116,7 @@ mod test {
use libm::powf;
#[test]
pub fn manual_camera() {
fn manual_camera() {
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)
@@ -154,7 +154,7 @@ mod test {
}
#[test]
pub fn point_outside_camera() {
fn point_outside_camera() {
// Scale 250 for an image 1000 x 1000 gives an effective range of [-2, 2]
let camera = Camera::new(
uvec2(1000, 1000),
@@ -169,7 +169,7 @@ mod test {
}
#[test]
pub fn point_outside_camera_negative() {
fn point_outside_camera_negative() {
// Scale 250 for an image 1000 x 1000 gives an effective range of [-2, 2]
let camera = Camera::new(
uvec2(1000, 1000),
@@ -184,7 +184,7 @@ mod test {
}
#[test]
pub fn aspect_ratio() {
fn 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(
+79
View File
@@ -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]);
}
}
+1
View File
@@ -8,6 +8,7 @@ pub mod chaos_game;
mod rng;
pub mod transform;
pub mod variation;
pub mod image;
use glam::Affine2;
+2
View File
@@ -14,6 +14,8 @@ use rand_xoshiro::Xoshiro256StarStar;
/// This function assumes a properly-initialized state array;
/// output may silently degenerate if the initial state is all zeros,
/// so this module is private to the crate.
// Temporarily unused, will be required once the main image accumulation entry point is implemented
#[allow(unused)]
pub(crate) fn xoshiro256starstar_from_seed(
rng_state: <Xoshiro256StarStar as SeedableRng>::Seed,
) -> Xoshiro256StarStar {
+1 -1
View File
@@ -125,6 +125,6 @@ mod test {
assert_eq!(transform.transform_color(0.0), 0.5);
assert_eq!(transform.transform_color(1.0), 1.0);
assert_eq!(transform.transform_color(2.0), 1.0);
assert_eq!(transform.transform_color(2.0), 1.5);
}
}