sprite_gen/
lib.rs

1#![crate_name = "sprite_gen"]
2
3use hsl::HSL;
4use randomize::{formulas, PCG32};
5
6/// Replacement for the `i8` datatype that can be passed to `gen_sprite`.
7#[derive(Debug, Clone, Eq, PartialEq)]
8pub enum MaskValue {
9    /// - `-1`: This pixel will always be a border.
10    Solid,
11    /// - `0`: This pixel will always be empty.
12    Empty,
13    /// - `1`: This pixel will either be empty or filled (body).
14    Body1,
15    /// - `2`: This pixel will either be a border or filled (body).
16    Body2,
17}
18
19impl MaskValue {
20    pub fn i8(&self) -> i8 {
21        match self {
22            MaskValue::Solid => -1,
23            MaskValue::Empty => 0,
24            MaskValue::Body1 => 1,
25            MaskValue::Body2 => 2,
26        }
27    }
28}
29
30impl From<MaskValue> for i8 {
31    fn from(from: MaskValue) -> Self {
32        from.i8()
33    }
34}
35
36impl From<i8> for MaskValue {
37    fn from(from: i8) -> Self {
38        match from {
39            -1 => MaskValue::Solid,
40            1 => MaskValue::Body1,
41            2 => MaskValue::Body2,
42            _ => MaskValue::Empty,
43        }
44    }
45}
46
47impl Default for MaskValue {
48    fn default() -> Self {
49        MaskValue::Empty
50    }
51}
52
53/// The options for the `gen_sprite` function.
54#[derive(Debug, Copy, Clone)]
55pub struct Options {
56    /// `true` if the result buffer should be mirrored along the X axis.
57    pub mirror_x: bool,
58    /// `true` if the result buffer should be mirrored along the Y axis.
59    pub mirror_y: bool,
60    /// `true` if the output should be colored. `false` if the output should be 1-bit. The
61    /// Fields after this field only apply if `colored` is `true`.
62    pub colored: bool,
63    /// A value from `0.0` - `1.0`.
64    pub edge_brightness: f32,
65    /// A value from `0.0` - `1.0`.
66    pub color_variations: f32,
67    /// A value from `0.0` - `1.0`.
68    pub brightness_noise: f32,
69    /// A value from `0.0` - `1.0`.
70    pub saturation: f32,
71    /// The seed for the random generator.
72    pub seed: u64,
73}
74
75impl Default for Options {
76    /// - `mirror_x`: `false`
77    /// - `mirror_y`: `false`
78    /// - `colored`: `true`
79    /// - `edge_brightness`: `0.3`
80    /// - `color_variations`: `0.2`
81    /// - `brightness_noise`: `0.3`
82    /// - `saturation`: `0.5`
83    /// - `seed`: `0`
84    fn default() -> Self {
85        Options {
86            mirror_x: false,
87            mirror_y: false,
88            colored: true,
89            edge_brightness: 0.3,
90            color_variations: 0.2,
91            brightness_noise: 0.3,
92            saturation: 0.5,
93            seed: 0,
94        }
95    }
96}
97
98/// Randomly generate a new sprite.
99///
100/// A mask buffer of `i8` values should be passed together with the width of that buffer.
101/// The height is automatically calculated by dividing the size of the buffer with the width.
102/// The `i8` values should be one of the following, and will generate a bitmap:
103/// - `-1`: This pixel will always be a border.
104/// - `0`: This pixel will always be empty.
105/// - `1`: This pixel will either be empty or filled (body).
106/// - `2`: This pixel will either be a border or filled (body).
107///
108/// ```
109/// use sprite_gen::{gen_sprite, Options, MaskValue};
110///
111/// let mask = vec![MaskValue::Empty; 12 * 12];
112/// let buffer = gen_sprite(&mask, 12, Options::default());
113/// ```
114pub fn gen_sprite<T>(mask_buffer: &[T], mask_width: usize, options: Options) -> Vec<u32>
115where
116    T: Into<i8> + Clone,
117{
118    let mask_height = mask_buffer.len() / mask_width;
119
120    // Copy the array to this vector
121    let mut mask: Vec<i8> = mask_buffer
122        .iter()
123        .map(|v| std::convert::Into::into(v.clone()))
124        .collect::<_>();
125
126    let mut rng = PCG32::seed(options.seed, 5);
127
128    // Generate a random sample, if it's a internal body there is a 50% chance it will be empty
129    // If it's a regular body there is a 50% chance it will turn into a border
130    for val in mask.iter_mut() {
131        if *val == 1 {
132            // Either 0 or 1
133            *val = formulas::f32_closed(rng.next_u32()).round() as i8;
134        } else if *val == 2 {
135            // Either -1 or 1
136            *val = formulas::f32_closed_neg_pos(rng.next_u32()).signum() as i8;
137        }
138    }
139
140    // Generate edges
141    for y in 0..mask_height {
142        for x in 0..mask_width {
143            let index = x + y * mask_width;
144            if mask[index] <= 0 {
145                continue;
146            }
147
148            if y > 0 && mask[index - mask_width] == 0 {
149                mask[index - mask_width] = -1;
150            }
151            if y < mask_height - 1 && mask[index + mask_width] == 0 {
152                mask[index + mask_width] = -1;
153            }
154            if x > 0 && mask[index - 1] == 0 {
155                mask[index - 1] = -1;
156            }
157            if x < mask_width - 1 && mask[index + 1] == 0 {
158                mask[index + 1] = -1;
159            }
160        }
161    }
162
163    // Color the mask image
164    let colored: Vec<u32> = if options.colored {
165        color_output(&mask, (mask_width, mask_height), &options, &mut rng)
166    } else {
167        onebit_output(&mask)
168    };
169
170    // Check for mirroring
171    if options.mirror_x && options.mirror_y {
172        // Mirror both X & Y
173        let width = mask_width * 2;
174        let height = mask_height * 2;
175        let mut result = vec![0; width * height];
176
177        for y in 0..mask_height {
178            for x in 0..mask_width {
179                let index = x + y * mask_width;
180                let value = colored[index];
181
182                let index = x + y * width;
183                result[index] = value;
184
185                let index = (width - x - 1) + y * width;
186                result[index] = value;
187
188                let index = x + (height - y - 1) * width;
189                result[index] = value;
190
191                let index = (width - x - 1) + (height - y - 1) * width;
192                result[index] = value;
193            }
194        }
195
196        return result;
197    } else if options.mirror_x {
198        // Only mirror X
199        let width = mask_width * 2;
200        let mut result = vec![0; width * mask_height];
201
202        for y in 0..mask_height {
203            for x in 0..mask_width {
204                let index = x + y * mask_width;
205                let value = colored[index];
206
207                let index = x + y * width;
208                result[index] = value;
209
210                let index = (width - x - 1) + y * width;
211                result[index] = value;
212            }
213        }
214
215        return result;
216    } else if options.mirror_y {
217        // Only mirror Y
218        let height = mask_height * 2;
219        let mut result = vec![0; mask_width * height];
220
221        for y in 0..mask_height {
222            for x in 0..mask_width {
223                let index = x + y * mask_width;
224                let value = colored[index];
225                result[index] = value;
226
227                let index = x + (height - y - 1) * mask_width;
228                result[index] = value;
229            }
230        }
231
232        return result;
233    }
234
235    colored
236}
237
238#[inline]
239fn onebit_output(mask: &[i8]) -> Vec<u32> {
240    mask.iter()
241        .map(|&v| match v {
242            -1 => 0,
243            _ => 0xFF_FF_FF_FF,
244        })
245        .collect()
246}
247
248#[inline]
249fn color_output(
250    mask: &[i8],
251    mask_size: (usize, usize),
252    options: &Options,
253    rng: &mut PCG32,
254) -> Vec<u32> {
255    let mut result = vec![0xFF_FF_FF_FF; mask.len()];
256
257    let is_vertical_gradient = formulas::f32_closed_neg_pos(rng.next_u32()) > 0.0;
258    let saturation = formulas::f32_closed(rng.next_u32()) * options.saturation;
259    let mut hue = formulas::f32_closed(rng.next_u32());
260
261    let variation_check = 1.0 - options.color_variations;
262    let brightness_inv = 1.0 - options.brightness_noise;
263
264    let uv_size = if is_vertical_gradient {
265        (mask_size.1, mask_size.0)
266    } else {
267        mask_size
268    };
269
270    for u in 0..uv_size.0 {
271        // Create a non-uniform random number being constrained more to the center (0)
272        let is_new_color = (formulas::f32_closed(rng.next_u32())
273            + formulas::f32_closed(rng.next_u32())
274            + formulas::f32_closed(rng.next_u32()))
275            / 3.0;
276
277        if is_new_color > variation_check {
278            hue = formulas::f32_closed(rng.next_u32());
279        }
280
281        let u_sin = ((u as f32 / uv_size.0 as f32) * std::f32::consts::PI).sin();
282
283        for v in 0..uv_size.1 {
284            let index = if is_vertical_gradient {
285                v + u * mask_size.0
286            } else {
287                u + v * mask_size.0
288            };
289
290            let val = mask[index];
291            if val == 0 {
292                continue;
293            }
294
295            let brightness = u_sin * brightness_inv
296                + formulas::f32_closed(rng.next_u32()) * options.brightness_noise;
297
298            let mut rgb = HSL {
299                h: hue as f64 * 360.0,
300                s: saturation as f64,
301                l: brightness as f64,
302            }
303            .to_rgb();
304
305            // Make the edges darker
306            if val == -1 {
307                rgb.0 = (rgb.0 as f32 * options.edge_brightness) as u8;
308                rgb.1 = (rgb.1 as f32 * options.edge_brightness) as u8;
309                rgb.2 = (rgb.2 as f32 * options.edge_brightness) as u8;
310            }
311
312            result[index] = ((rgb.0 as u32) << 16) | ((rgb.1 as u32) << 8) | (rgb.2 as u32);
313        }
314    }
315
316    result
317}