aboutsummaryrefslogtreecommitdiff
path: root/src/lib.rs
blob: 3d7cf2df60fd8f9690d33c707b0cc8dc3ab1271e7d1d6a47ce45fb4721c4e4c8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
/*
    hitomezashi-rs  Classic two-colored Hitomezashi stitch pattern generator
    Copyright (C) 2024  Nicholas Johnson

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

#![warn(missing_docs)]

//! Classic two-colored Hitomezashi stitch pattern generator.

use rand::distributions::{Bernoulli, Distribution};
use std::io::{stdout, BufWriter, StdoutLock, Write};

const TRANSPARENT_SQUARE: &str = " ";
const OPAQUE_SQUARE: &str = "█";

fn print_square(stream: &mut BufWriter<StdoutLock>, is_opaque: bool) {
    stream
        .write_all(
            if is_opaque {
                OPAQUE_SQUARE
            } else {
                TRANSPARENT_SQUARE
            }
            .to_string()
            .as_bytes(),
        )
        .unwrap();
}

/// Prints a two-colored Hitomezashi stitch pattern of the specified dimensions.
///
/// `skew` is the probability of a row or column beginning with a stitch. Skew values near 0 or 1
/// generate orderly patterns. Skew values near 0.5 generate chaotic patterns.
///
/// # Panics
///
/// This function will panic if any of the following constraints are not met:
///
/// `width >= 1`
///
/// `height >= 1`
///
/// `0 <= skew <= 1`
///
/// # Examples
///
/// ```
/// use hitomezashi_rs;
///
/// hitomezashi_rs::generate(15, 20, Some(0.7));
/// ```
pub fn generate(width: usize, height: usize, skew: Option<f64>) {
    // skew=0.5 generates the most random-looking patterns
    let skew = skew.unwrap_or(0.5);

    assert!(width >= 1, "Width must be a positive number!");
    assert!(height >= 1, "Height must be a positive number!");
    assert!(
        (0.0..=1.0).contains(&skew),
        "Skew must be between zero and one inclusive!"
    );

    let mut rng = rand::thread_rng();
    let brn = Bernoulli::new(skew).unwrap();

    // Each value represents whether to start that row with a stitch or no stitch
    let mut row_bits: Vec<bool> = Vec::with_capacity(height - 1);

    for _ in 0..row_bits.capacity() {
        row_bits.push(brn.sample(&mut rng));
    }

    // Each value represents whether to start that column with a stitch or no stitch
    let mut col_bits: Vec<bool> = Vec::with_capacity(width - 1);

    for _ in 0..col_bits.capacity() {
        col_bits.push(brn.sample(&mut rng));
    }

    // Precomputed values to facilitate alternating between stitch presence and stitch absence
    let mut alt_bits: Vec<bool> = Vec::with_capacity(width);

    let mut alternator = false;
    for _ in 0..alt_bits.capacity() {
        alt_bits.push(alternator);
        alternator = !alternator;
    }

    /* The first square is always opaque to prevent invisible 1xN or Nx1 patterns. Invisible
     * patterns must be prevented so that users don't mistakenly conclude that the function does
     * not work. */
    let first_square = true;

    let mut cur_row: Vec<bool> = Vec::with_capacity(width);

    let lock = stdout().lock();
    let mut stream = BufWriter::new(lock);

    /* Base case
     *
     * The first row has no preceding row, so it's computing using the column stitches */
    cur_row.push(first_square);
    for col in 0..(cur_row.capacity() - 1) {
        print_square(&mut stream, cur_row[col]);
        cur_row.push(cur_row[col] ^ col_bits[col]);
    }

    print_square(&mut stream, cur_row[cur_row.capacity() - 1]);
    stream.write_all(b"\n").unwrap(); // End of base case

    for row_bit in row_bits.iter() {
        /* Recursive case
         *
         * Each row after the first row is computed based on the preceding row. */
        cur_row
            .iter_mut()
            .zip(alt_bits.iter())
            .for_each(|(x1, &x2)| {
                *x1 ^= x2 ^ row_bit;
                print_square(&mut stream, *x1);
            });

        stream.write_all(b"\n").unwrap(); // End of recursive case
    }

    stream.flush().unwrap();
}