• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cookiephone / libnoise-rs / #39

28 Apr 2025 08:21AM UTC coverage: 89.343% (-4.1%) from 93.457%
#39

push

web-flow
Merge pull request #16 from cookiephone/2024edition

update rust edition to 2024

64 of 67 new or added lines in 7 files covered. (95.52%)

46 existing lines in 6 files now uncovered.

1006 of 1126 relevant lines covered (89.34%)

549094.37 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

74.71
/src/core/utils/visualizer.rs
1
use crate::core::generator::Generator;
2
use crate::core::utils::noisebuf::NoiseBuffer;
3
use image::{
4
    ColorType, GrayImage, ImageError,
5
    codecs::gif::{GifEncoder, Repeat},
6
};
7
use itertools::Itertools;
8
use std::{
9
    fs::OpenOptions,
10
    io::Error,
11
    ops::{Index, IndexMut},
12
};
13

14
/// A struct for visualizing the output of a generator.
15
///
16
/// The `image` feature must be enabled for writing images of visualizations.
17
///
18
/// This struct represents a simple way to quickly visualize the output of a [`Generator`] by
19
/// building a [`NoiseBuffer`] of a given size, populating it with data, and creating an PNG or
20
/// GIF file visualizing said data.
21
///
22
/// In the 1D case, the visualization is a grayscale band of one pixel in height and with the
23
/// provided length. In the 2D case, the visualization is an image of the provided dimensions.
24
/// In the 3D case, the visualization is an image providing an isometric view on a cube
25
/// representing the buffer. In the 4D case, the visualization is equivalent to the 3D case,
26
/// except the result is an animation with the 4th dimension mapping to time.
27
///
28
/// <p style="background:rgba(122,186,255,0.16);padding:0.75em;">
29
/// <strong>Note:</strong>
30
/// This struct is meant to be used to get an idea of what a generator is doing. Especially the
31
/// 1D, 3D, and 4D cases are not suited for usage besides debugging, as the main goal of this
32
/// library is to provide an efficient and modular way to creating a noise generation pipeline.
33
/// </p>
34
///
35
/// The usage of this struct is simple and analogous to that of [`NoiseBuffer`]:
36
///
37
/// ```
38
/// # use libnoise::{Source, NoiseBuffer, Visualizer};
39
/// # use tempdir::TempDir;
40
/// // create a generator
41
/// let generator = Source::simplex(42);
42
///
43
/// // create a visualizer and use it to visualize the output of the generator
44
/// let path = "output.png";
45
/// # let tmp_dir = TempDir::new("libnoise").unwrap();
46
/// # let path = &tmp_dir.path().join(path).into_os_string().into_string().unwrap();
47
/// Visualizer::<3>::new([30, 20, 25], &generator).write_to_file(path).unwrap();
48
/// ```
49
///
50
/// In fact, a visualizer can be created from a [`NoiseBuffer`] by simply converting it
51
/// to a [`Visualizer`]:
52
///
53
/// ```
54
/// # use libnoise::{Source, NoiseBuffer, Visualizer};
55
/// # use tempdir::TempDir;
56
/// // create a generator
57
/// let generator = Source::simplex(42);
58
///
59
/// // create a noise buffer
60
/// let buf = NoiseBuffer::<3>::new([30, 20, 25], &generator);
61
///
62
/// // create a visualizer and use it to visualize the output of the generator
63
/// let path = "output.png";
64
/// # let tmp_dir = TempDir::new("libnoise").unwrap();
65
/// # let path = &tmp_dir.path().join(path).into_os_string().into_string().unwrap();
66
/// Visualizer::from(buf).write_to_file(path);
67
/// ```
68
#[derive(Clone, Debug)]
69
pub struct Visualizer<const D: usize> {
70
    /// Stores the length of the underlying n-dimensional array along each dimension.
71
    shape: [usize; D],
72
    /// Stores offsets which are used to convert n-dimensional coordinates to flat vector indices.
73
    offsets: [usize; D],
74
    /// The underlying flat vector storing the noise values as `u8` integers.
75
    pixel_buffer: Vec<u8>,
76
}
77

78
impl<const D: usize> Index<&[usize]> for Visualizer<D> {
79
    type Output = u8;
80
    fn index(&self, index: &[usize]) -> &Self::Output {
1,664,000✔
81
        let idx = self.flat_index(index);
1,664,000✔
82
        &self.pixel_buffer[idx]
1,664,000✔
83
    }
84
}
85

86
impl<const D: usize> IndexMut<&[usize]> for Visualizer<D> {
87
    fn index_mut(&mut self, index: &[usize]) -> &mut Self::Output {
×
88
        let idx = self.flat_index(index);
×
89
        &mut self.pixel_buffer[idx]
×
90
    }
91
}
92

93
impl<const D: usize> From<NoiseBuffer<D>> for Visualizer<D> {
94
    fn from(noisebuf: NoiseBuffer<D>) -> Self {
1,024✔
95
        Self {
96
            shape: noisebuf.shape,
1,024✔
97
            offsets: noisebuf.offsets,
1,024✔
98
            pixel_buffer: noisebuf.buffer.into_iter().map(norm_to_u8).collect(),
1,024✔
99
        }
100
    }
101
}
102

103
impl<const D: usize> Visualizer<D> {
104
    fn flat_index(&self, index: &[usize]) -> usize {
1,664,000✔
105
        index
1,664,000✔
106
            .iter()
107
            .zip(&self.offsets)
1,664,000✔
108
            .map(|(idx, offset)| idx * offset)
8,770,560✔
109
            .sum()
110
    }
111
}
112

113
impl Visualizer<1> {
114
    /// Creates a new [`Visualizer`] with the given `shape` and filled with noise generated
115
    /// by the given `generator`. For further detail see the
116
    /// [struct-level documentation](Visualizer).
117
    pub fn new<G: Generator<1>>(shape: [usize; 1], generator: &G) -> Self {
256✔
118
        NoiseBuffer::<1>::new(shape, generator).into()
256✔
119
    }
120

121
    /// Write a PNG file to the given `path`, visualizing the output of the provided
122
    /// generator. For further detail see the [struct-level documentation](Visualizer).
123
    pub fn write_to_file(&self, path: &str) -> Result<(), ImageError> {
256✔
124
        let image =
256✔
125
            GrayImage::from_raw(self.shape[0] as u32, 1, self.pixel_buffer.clone()).unwrap();
256✔
126
        image.save(path)?;
256✔
127
        Ok(())
256✔
128
    }
129
}
130

131
impl Visualizer<2> {
132
    /// Creates a new [`Visualizer`] with the given `shape` and filled with noise generated
133
    /// by the given `generator`. For further detail see the
134
    /// [struct-level documentation](Visualizer).
135
    pub fn new<G: Generator<2>>(shape: [usize; 2], generator: &G) -> Self {
256✔
136
        NoiseBuffer::<2>::new(shape, generator).into()
256✔
137
    }
138

139
    pub fn write_to_file(&self, path: &str) -> Result<(), ImageError> {
256✔
140
        let image = GrayImage::from_raw(
141
            self.shape[1] as u32,
256✔
142
            self.shape[0] as u32,
256✔
143
            self.pixel_buffer.clone(),
256✔
144
        )
145
        .unwrap();
146
        image.save(path)?;
256✔
147
        Ok(())
256✔
148
    }
149
}
150

151
impl Visualizer<3> {
152
    /// Creates a new [`Visualizer`] with the given `shape` and filled with noise generated
153
    /// by the given `generator`. For further detail see the
154
    /// [struct-level documentation](Visualizer).
155
    pub fn new<G: Generator<3>>(shape: [usize; 3], generator: &G) -> Self {
256✔
156
        NoiseBuffer::<3>::new(shape, generator).into()
256✔
157
    }
158

159
    /// Write a PNG file to the given `path`, visualizing the output of the provided
160
    /// generator. For further detail see the [struct-level documentation](Visualizer).
161
    pub fn write_to_file(&self, path: &str) -> Result<(), ImageError> {
256✔
162
        let scale = 0.45;
256✔
163
        let center = (self.shape[0] as f64 * 0.5, self.shape[1] as f64 * 0.5);
256✔
164
        let mut buf = vec![0; self.shape[0] * self.shape[1]];
256✔
165
        for z_idx in (0..self.shape[2]).rev() {
7,936✔
166
            for p in tensor_indices(&[self.shape[0], self.shape[1]]) {
6,912,000✔
167
                if let Some(buf_idx) =
1,213,440✔
UNCOV
168
                    xyz_screen_to_buff_indices(p[0], p[1], z_idx, center.0, center.1, scale)
×
169
                {
UNCOV
170
                    buf[p[0] * self.shape[1] + p[1]] = self[&[buf_idx.0, buf_idx.1, buf_idx.2]];
×
171
                }
172
            }
173
        }
174

175
        let image = GrayImage::from_raw(self.shape[1] as u32, self.shape[0] as u32, buf).unwrap();
256✔
176
        image.save(path)?;
256✔
177
        Ok(())
256✔
178
    }
179
}
180

181
impl Visualizer<4> {
182
    /// Creates a new [`Visualizer`] with the given `shape` and filled with noise generated
183
    /// by the given `generator`. For further detail see the
184
    /// [struct-level documentation](Visualizer).
185
    pub fn new<G: Generator<4>>(shape: [usize; 4], generator: &G) -> Self {
256✔
186
        NoiseBuffer::<4>::new(shape, generator).into()
256✔
187
    }
188

189
    /// Write a GIF file to the given `path`, visualizing the output of the provided
190
    /// generator. For further detail see the [struct-level documentation](Visualizer).
191
    pub fn write_to_file(&self, path: &str) -> Result<(), Error> {
256✔
192
        let file_out = OpenOptions::new()
512✔
193
            .write(true)
194
            .truncate(true)
195
            .create(true)
196
            .open(path)?;
256✔
197

UNCOV
198
        let mut encoder = GifEncoder::new(file_out);
×
UNCOV
199
        encoder.set_repeat(Repeat::Infinite).unwrap();
×
200

UNCOV
201
        let scale = 0.45;
×
UNCOV
202
        let center = (self.shape[0] as f64 * 0.5, self.shape[1] as f64 * 0.5);
×
203
        for t in 0..self.shape[3] {
2,560✔
UNCOV
204
            let mut buf = vec![0; self.shape[0] * self.shape[1]];
×
205
            for z_idx in (0..self.shape[2]).rev() {
25,600✔
206
                for p in tensor_indices(&[self.shape[0], self.shape[1]]) {
2,560,000✔
207
                    if let Some(buf_idx) =
450,560✔
UNCOV
208
                        xyz_screen_to_buff_indices(p[0], p[1], z_idx, center.0, center.1, scale)
×
209
                    {
UNCOV
210
                        buf[p[0] * self.shape[0] + p[1]] =
×
UNCOV
211
                            self[&[buf_idx.0, buf_idx.1, buf_idx.2, t]];
×
212
                    }
213
                }
214
            }
215

UNCOV
216
            buf = buf
×
UNCOV
217
                .into_iter()
×
218
                .flat_map(|val| std::iter::repeat_n(val, 3))
256,000✔
UNCOV
219
                .collect();
×
220

UNCOV
221
            encoder
×
222
                .encode(
UNCOV
223
                    &buf,
×
UNCOV
224
                    self.shape[0] as u32,
×
UNCOV
225
                    self.shape[1] as u32,
×
UNCOV
226
                    ColorType::Rgb8,
×
227
                )
228
                .unwrap();
229
        }
UNCOV
230
        Ok(())
×
231
    }
232
}
233

234
pub(crate) fn norm_to_u8(x: f64) -> u8 {
12,288,000✔
235
    (127.5 + x * 127.5) as u8
12,288,000✔
236
}
237

238
fn xyz_screen_to_buff_indices(
9,472,000✔
239
    x: usize,
240
    y: usize,
241
    z: usize,
242
    center_x: f64,
243
    center_y: f64,
244
    scale: f64,
245
) -> Option<(usize, usize, usize)> {
246
    let mut x = x as f64;
9,472,000✔
247
    let mut y = y as f64;
9,472,000✔
248
    x -= center_x * (1.0 - scale) + scale * z as f64;
9,472,000✔
249
    y -= center_y;
9,472,000✔
250
    let xx = -(x + y / 3_f64.sqrt());
9,472,000✔
251
    let yy = 2.0 * y / 3_f64.sqrt() + xx;
9,472,000✔
252
    x = xx / scale + center_x;
9,472,000✔
253
    y = yy / scale + center_y;
9,472,000✔
254
    if x < 0.0 || y < 0.0 || x >= 2.0 * center_x || y >= 2.0 * center_y {
24,934,656✔
255
        None
7,808,000✔
256
    } else {
257
        Some((x as usize, y as usize, z))
1,664,000✔
258
    }
259
}
260

261
fn tensor_indices(shape: &[usize]) -> impl Iterator<Item = Vec<usize>> + use<> {
33,280✔
262
    shape
33,280✔
263
        .iter()
264
        .map(|&dim_size| 0..dim_size)
133,120✔
265
        .multi_cartesian_product()
266
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc