wood_grain/
lib.rs

1#![warn(clippy::pedantic, clippy::nursery)]
2#![cfg_attr(not(test), no_std)]
3#[cfg(test)]
4mod tests {
5    use super::*;
6    #[test]
7    fn rawwood_12() {
8        for i in 0..5 {
9            let raw_wood = wood(584, 668, 40., 12., &WOOD_1);
10            raw_wood
11                .unwrap()
12                .save(format!("rawwood_12_{}.png", i))
13                .unwrap();
14        }
15    }
16    #[test]
17    fn brightwood12() {
18        for i in 0..5 {
19            let bright = wood(584, 668, 40., 12., &BRIGHT_WOOD);
20            bright
21                .unwrap()
22                .save(format!("brightwood_12_{}.png", i))
23                .unwrap();
24        }
25    }
26
27    #[test]
28    fn rawwood_24() {
29        for i in 0..5 {
30            let raw_wood = wood(584, 668, 40., 24., &WOOD_1);
31            raw_wood
32                .unwrap()
33                .save(format!("rawwood_24_{}.png", i))
34                .unwrap();
35        }
36    }
37}
38
39extern crate alloc;
40use alloc::vec::Vec;
41
42struct Noise {
43    width: usize,
44    height: usize,
45    data: Vec<Vec<f64>>,
46}
47
48use rand::distributions::{Distribution, Uniform};
49
50impl Noise {
51    fn gen_noise(width: usize, height: usize) -> Self {
52        /* algorithm taken from https://lodev.org/cgtutor/randomnoise.html#Wood */
53        let between = Uniform::from(0.0..1.0);
54        let mut rng = rand::thread_rng();
55        let mut noise: Vec<Vec<f64>> = Vec::new();
56        for _ in 0..height {
57            let mut vec = Vec::new();
58            for _ in 0..width {
59                vec.push(between.sample(&mut rng));
60            }
61            noise.push(vec);
62        }
63
64        Self {
65            width,
66            height,
67            data: noise,
68        }
69    }
70
71    fn sample_smooth_noise(&self, x: f64, y: f64) -> f64 {
72        /* algorithm taken from https://lodev.org/cgtutor/randomnoise.html#Wood */
73        let fract_x = x.fract();
74        let fract_y = y.fract();
75        let width = self.width;
76        let height = self.height;
77
78        //wrap around
79        let x1: usize = ((x as usize) + width) % width;
80        let y1: usize = ((y as usize) + height) % height;
81
82        //neighbor values
83        let x2: usize = (x1 + width - 1) % width;
84        let y2: usize = (y1 + height - 1) % height;
85
86        //smooth the noise with bilinear interpolation
87        let mut value = 0.0;
88        value += fract_x * fract_y * self.data[y1][x1];
89        value += (1. - fract_x) * fract_y * self.data[y1][x2];
90        value += fract_x * (1. - fract_y) * self.data[y2][x1];
91        value += (1. - fract_x) * (1. - fract_y) * self.data[y2][x2];
92
93        value
94    }
95
96    fn turbulence(&self, x: f64, y: f64, initial_size: f64) -> f64 {
97        /* algorithm taken from https://lodev.org/cgtutor/randomnoise.html#Wood */
98        let mut value = 0.0_f64;
99        let mut size = initial_size;
100
101        while size >= 1. {
102            value += self.sample_smooth_noise(x / size, y / size) * size;
103            size /= 2.0;
104        }
105
106        128.0 * value / initial_size
107    }
108}
109
110pub struct WoodProfile {
111    brightness_adjustment: i32,
112    dark_color: [u8; 3],
113    light_color: [u8; 3],
114}
115
116pub const BRIGHT_WOOD: WoodProfile = WoodProfile {
117    brightness_adjustment: 20,
118    dark_color: [120, 70, 70],
119    light_color: [208, 158, 70],
120};
121
122pub const WOOD_1: WoodProfile = WoodProfile {
123    brightness_adjustment: 0,
124    dark_color: [120, 70, 70],
125    light_color: [208, 158, 70],
126};
127
128/// * `width`: width of the image to be generated
129/// * `height`: height of the image to be generated
130/// * `offsetstdev`: signifies how large the offset should be (the center of the wood grain is randomly shifted in the x and y directions).
131/// * `length_scale`: denotes the average length of spacing between grains in pixels.
132///
133/// # Errors
134/// Returns `BadVariance` error if `offsetstdev` is infinite.
135#[must_use]
136pub fn wood(
137    width: u32,
138    height: u32,
139    offsetstdev: f64,
140    length_scale: f64,
141    wood_profile: &WoodProfile,
142) -> Result<image::RgbImage, rand_distr::NormalError> {
143    use rand::Rng;
144    let mut imgbuf = image::RgbImage::new(width, height);
145
146    let noise = Noise::gen_noise(width as usize, height as usize);
147
148    /* algorithm taken and modified from https://lodev.org/cgtutor/randomnoise.html#Wood */
149    let turb = 14.6; //makes twists
150    let turb_size = 32.0; //initial size of the turbulence
151
152    let mut rng = rand::thread_rng();
153    let distr = rand_distr::Normal::new(0., offsetstdev)?;
154    let offset_x = rng.sample(distr);
155    let offset_y = rng.sample(distr);
156
157    // There is an abs later in the function, so we only need from 0 to pi.
158    let phase = rng.sample(Uniform::from(0.0..core::f64::consts::PI));
159
160    for (x, y, pixel) in imgbuf.enumerate_pixels_mut() {
161        let x_value_times_scale = f64::from(x) - f64::from(width) / 2. + offset_x; // dimension: px
162        let y_value_times_scale = f64::from(y) - f64::from(height) / 2. + offset_y; // dimension: px
163        let dist_value_times_scale = x_value_times_scale.hypot(y_value_times_scale)
164            + turb * noise.turbulence(f64::from(x), f64::from(y), turb_size) / 256.0;
165
166        #[allow(clippy::cast_possible_truncation)]
167        let sine_value = (dist_value_times_scale / length_scale)
168            .mul_add(core::f64::consts::PI, phase)
169            .sin()
170            .abs()
171            .powf(0.4) as f32;
172        *pixel = lerp_pixel(
173            image::Rgb(wood_profile.dark_color),
174            image::Rgb(wood_profile.light_color),
175            sine_value,
176        );
177    }
178
179    Ok(image::imageops::colorops::brighten(
180        &imgbuf,
181        wood_profile.brightness_adjustment,
182    ))
183}
184
185use interpolation::Lerp;
186
187fn lerp_pixel(a: image::Rgb<u8>, b: image::Rgb<u8>, t: f32) -> image::Rgb<u8> {
188    image::Rgb([
189        (a.0[0]).lerp(&b.0[0], &t),
190        (a.0[1]).lerp(&b.0[1], &t),
191        (a.0[2]).lerp(&b.0[2], &t),
192    ])
193}