making the code public

This commit is contained in:
cqql 2024-06-30 11:16:25 +02:00
commit eacbf23265
6 changed files with 2014 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
/target
/plots
/notes
.vscode/

1519
Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

16
Cargo.toml Normal file
View file

@ -0,0 +1,16 @@
[package]
name = "frost_patterns"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.5.7", features = ["derive", "cargo"] }
draw = "0.3.0"
hex = "0.4.3"
image = "0.25.1"
rand = "0.8.5"
rand_chacha = "0.3.1"
regex = "1.10.5"

70
README.md Normal file
View file

@ -0,0 +1,70 @@
# Frost Patterns
#### Making Diffusion Limited Aggregation look nice
![a result of the DLA algorithm where a cell is a light blue square and empty space is very dark blue, blurred slightly, with the unblurred result overlaid on top in purple](https://cqql.site/assets/articles/frost_patterns/gallery_5.png)
I learned about an algorithm called [Diffusion-Limited Aggregation](https://en.wikipedia.org/wiki/Diffusion-limited_aggregation) from [this youtube video](https://www.youtube.com/watch?v=gsJHzBTPG0Y) about procedural landscape generation for games. It happens on an empty grid with one cell in the middle. Let's call that cell the shape for now. Then, in each step of the algorithm, a new cell is spawned in a random location and does a random walk until it touches the side of the shape, after which it stays there and becomes a part of the shape.
I wrote more about how I made it on my website in the [Frost Patterns](https://cqql.site/articles/frost_patterns.html) article.
## This repository
This is the code I wrote for generating those images. It builds into a binary that can be used to generate an image with any initialization parameters. The result can either be saved to a file, or written to stdout to pipe into other programs.
### Usage
Here's how to use it:
```
Usage: frost_patterns [OPTIONS] --width <grid width> --height <grid height> --num-particles <number of particles> --scale <image scale> --palette <123456-abcdef-000000>
Options:
--width <grid width>
The width of the grid on which particles will be placed
--height <grid height>
The height of the grid on which particles will be placed
--num-particles <number of particles>
The number of particles to place on the grid
--scale <image scale>
How much scaled up should the image be, i.e. the image width will be the grid width multiplied by the grid height
--seed <randomness seed>
Optional randomness seed for the generator - providing the same seed will generate the same result. Leave empty for a random value
--palette <123456-abcdef-000000>
Shorthand notation of the color palette to use. Three RGB values in hex, where the first is the background color, the second is the blurry shape color, and the third is the sharp overlaid shape color
--filename <FILENAME>
Filename to save the image as. If not provided, a descriptive filename will be generated. Can't be used together with -i
--cell-placement-strategy <CELL_PLACEMENT_STRATEGY>
[default: anywhere] [possible values: anywhere, corners]
-v
Verbose - prints the progress of generating the image and the filename it has been saved to. Can't be used together with -i
-i
Output PNG bytes to stdout. Can't be used together with -v or --filename
-h, --help
Print help
```
Cell placement strategy determines where the new cells will be placed before they start their random walk.
- `anywhere` - place the new cell anywhere there's an empty space but not right next to the shape.
- `corners` - place the new cell in any randomly picked corner of the grid that isn't occupied by the shape yet. If all corners are occupied, it falls back to `anywhere`.
Keeping a `--seed` value set will generate the same shape every time that seed is used, given the same grid size, number of particles, and cell placement strategy are used.
#### Example
Generate on a 100 by 100 grid, walk 2000 particles on it (so the shape will be made up of 2000 cells). Scale the grid by 5 to generate the final image (500 by 500 pixels). Use #e5f9e0 for the background color, #a3f7b5 for the gradient under the shape, and #40c9a2 for the shape. Print the progress of generating the image (`-v`).
```
./target/release/frost_patterns --width 100 --height 100 --num-particles 2000 --scale 5 --palette e5f9e0-a3f7b5-40c9a2 -v
```
### Building
To build the project, run:
```sh
cargo build --release
```
and the executable binary will be in `target/release/frost_patterns`.
### Attribution
Feel free to use and modify this software. I would appreciate if you could attribute me if you do (most preferrably by using my website [https://cqql.site](cqql.site)) and let me know, I'll be curious! But I don't require attribution. I do not consent for the software, images generated by it, or any derivative work of them, to be used for commercial purposes without my written permission. You can contact me by email at cqql.0@proton.me. I will be very open to talk, the only reason I'm going non-commercial is because I want to prevent someone from making a no-effort dropshipping business selling posters of my generative art projects.
The software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the software.

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

405
src/main.rs Normal file
View file

@ -0,0 +1,405 @@
use std::{
fmt,
io::{BufWriter, Cursor, Write},
};
use hex;
use image::{self, imageops, ImageError};
use rand::{thread_rng, Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use regex::Regex;
const OVERLAY_SPACING: f64 = 0.0;
#[derive(Clone)]
enum CellPlacementStrategy {
Corners,
Anywhere,
}
impl fmt::Display for CellPlacementStrategy {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
CellPlacementStrategy::Corners => write!(f, "corners"),
CellPlacementStrategy::Anywhere => write!(f, "anywhere"),
}
}
}
impl CellPlacementStrategy {
fn new(string: &str) -> Result<CellPlacementStrategy, String> {
match string {
"anywhere" => Ok(CellPlacementStrategy::Anywhere),
"corners" => Ok(CellPlacementStrategy::Corners),
_ => Err(format!(
"\"{}\" is not a valid option for CellPlacementStrategy",
string
)),
}
}
}
use clap::Parser;
#[derive(Parser)]
#[clap()]
struct Cli {
/// The width of the grid on which particles will be placed.
#[clap(long, value_name = "grid width")]
width: usize,
/// The height of the grid on which particles will be placed.
#[clap(long, value_name = "grid height")]
height: usize,
/// The number of particles to place on the grid
#[clap(long, value_name = "number of particles")]
num_particles: usize,
/// How much scaled up should the image be, i.e. the image
/// width will be the grid width multiplied by the grid height.
#[clap(long, value_name = "image scale")]
scale: usize,
/// Optional randomness seed for the generator - providing the same
/// seed will generate the same result. Leave empty for a random value.
#[clap(long, value_name = "randomness seed")]
seed: Option<u64>,
/// Shorthand notation of the color palette to use. Three RGB
/// values in hex, where the first is the background color, the
/// second is the blurry shape color, and the third is the sharp
/// overlaid shape color.
/* examples
ffdddd-b4e1ff-ab87ff
e5f9e0-a3f7b5-40c9a2
f2f3ae-edd382-fc9e4f
392f5a-f092dd-ffaff0
19535f-0b7a75-d7c9aa
32021f-006d77-e8f7ee
*/
#[clap(long, value_name = "123456-abcdef-000000")]
palette: String,
/// Filename to save the image as. If not provided, a descriptive
/// filename will be generated. Can't be used together with -i
#[clap(long)]
filename: Option<String>,
#[clap(long, default_value_t=String::from("anywhere"), value_parser=clap::builder::PossibleValuesParser::new(["anywhere", "corners"]))]
cell_placement_strategy: String,
/// Verbose - prints the progress of generating the image and
/// the filename it has been saved to. Can't be used together with -i
#[clap(short)]
v: bool,
/// Output PNG bytes to stdout. Can't be used together with -v or --filename
#[clap(short)]
i: bool,
}
fn main() {
let cli = Cli::parse();
let width = cli.width;
let height = cli.height;
let num_particles = cli.num_particles;
let scale = cli.scale;
let blur = 4.0 * scale as f32;
let strategy = match CellPlacementStrategy::new(cli.cell_placement_strategy.as_str()) {
Ok(strategy) => strategy,
Err(e) => panic!("{}", e),
};
let seed = match cli.seed {
Some(seed) => seed,
None => thread_rng().gen(),
};
let verbose = cli.v;
let write_to_stdout = cli.i;
if verbose && write_to_stdout {
panic!("Can't use -i and -v together");
}
let filename = cli.filename;
if write_to_stdout && filename.is_some() {
panic!("Can't use -i and --filename together");
}
let palette_str = cli.palette;
let palette_regex = Regex::new(r"([0-9a-fA-F]{6})-([0-9a-fA-F]{6})-([0-9a-fA-F]{6})").unwrap();
let palette: [&str; 3] = match palette_regex.captures(&palette_str) {
Some(captures) => captures.extract().1,
None => panic!("The color palette has to be 3 colors as 6 digit hex strings representing their RGB values"),
};
let grid = generate_grid(width, height, num_particles, seed, strategy, verbose);
let imgx = width * scale;
let imgy = height * scale;
let mut imgbuf: image::ImageBuffer<image::Rgb<u8>, Vec<u8>> =
image::ImageBuffer::new(imgx as u32, imgy as u32);
for x in 0..width {
for y in 0..height {
for img_x in (x * scale)..((x + 1) * scale) {
for img_y in (y * scale)..((y + 1) * scale) {
let pixel = imgbuf.get_pixel_mut(img_x as u32, img_y as u32);
*pixel = image::Rgb(match grid[x][y] {
true => [255u8, 255u8, 255u8],
false => [0u8, 0u8, 0u8],
});
}
}
}
}
let image = make_blurred_colored_overlaid(&imgbuf, &grid, &palette, scale, blur);
if write_to_stdout {
let mut buffer = Cursor::new(Vec::new());
{
let mut writer = BufWriter::new(&mut buffer);
match image.write_to(&mut writer, image::ImageFormat::Png) {
Ok(_) => {}
Err(e) => {
panic!("Failed to load image bytes: {}", e)
}
};
}
match std::io::stdout().write_all(&buffer.into_inner()) {
Ok(_) => {}
Err(e) => {
panic!("Failed to write image bytes to stdout: {}", e)
}
};
} else {
save_image(
image,
width,
height,
scale,
num_particles,
seed,
&palette_str,
filename,
verbose,
)
.unwrap();
}
}
fn make_blurred_colored_overlaid(
imgbuf: &image::ImageBuffer<image::Rgb<u8>, Vec<u8>>,
grid: &Vec<Vec<bool>>,
palette: &[&str; 3],
scale: usize,
blur: f32,
) -> image::ImageBuffer<image::Rgb<u8>, Vec<u8>> {
let width = grid.len();
let height = grid.get(0).unwrap().len();
let blurred = imageops::blur(imgbuf, blur);
let start_color = hex::decode(&palette[0]).unwrap().try_into().unwrap();
let end_color = hex::decode(&palette[1]).unwrap().try_into().unwrap();
let overlay_color = hex::decode(&palette[2]).unwrap().try_into().unwrap();
let mut blurred_colored_overlaid: image::ImageBuffer<image::Rgb<u8>, Vec<u8>> = blurred.clone();
blurred_colored_overlaid.pixels_mut().for_each(|pixel| {
*pixel = image::Rgb(make_gradient(
&start_color,
&end_color,
pixel[0] as f64 / 255.0,
))
});
for x in 0..width {
for y in 0..height {
if !grid[x][y] {
continue;
}
for img_x in (x * scale + (scale as f64 * OVERLAY_SPACING) as usize)
..((x + 1) * scale - (scale as f64 * OVERLAY_SPACING) as usize)
{
for img_y in (y * scale + (scale as f64 * OVERLAY_SPACING) as usize)
..((y + 1) * scale - (scale as f64 * OVERLAY_SPACING) as usize)
{
let pixel = blurred_colored_overlaid.get_pixel_mut(img_x as u32, img_y as u32);
*pixel = image::Rgb(overlay_color)
}
}
}
}
blurred_colored_overlaid
}
fn save_image(
image: image::RgbImage,
width: usize,
height: usize,
scale: usize,
num_particles: usize,
seed: u64,
palette: &String,
filename: Option<String>,
verbose: bool,
) -> Result<(), ImageError> {
let filename = match filename {
Some(filename) => filename,
None => format!(
"drawing-{}x{}s{}-p{}-s{}-p({}).png",
width, height, scale, num_particles, seed, palette
),
};
image.save(&filename)?;
if verbose {
println!("saved as {}", filename)
}
Ok(())
}
fn make_gradient(start: &[u8; 3], end: &[u8; 3], distance: f64) -> [u8; 3] {
let distance = if distance <= 0.0 {
0.0
} else if distance >= 1.0 {
1.0
} else {
distance
};
let mut result: [u8; 3] = [0, 0, 0];
for i in 0..3 {
result[i] = ((1.0 - distance) * start[i] as f64 + distance * end[i] as f64) as u8;
}
result
}
fn generate_grid(
width: usize,
height: usize,
num_particles: usize,
seed: u64,
cell_placement_strategy: CellPlacementStrategy,
verbose: bool,
) -> Vec<Vec<bool>> {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let mut grid: Vec<Vec<bool>> = vec![vec![false; height]; width];
grid[width / 2][height / 2] = true;
for particle_i in 0..num_particles {
let (mut x, mut y) =
random_cell_placement(cell_placement_strategy.clone(), &rng, &grid, width, height);
while !is_stuck((x, y), width, height, &grid) {
let diff_x: isize = rng.gen_range(-1..=1);
let diff_y: isize = rng.gen_range(-1..=1);
if x + diff_x < width as isize
&& x + diff_x >= 0
&& y + diff_y < height as isize
&& y + diff_y >= 0
&& !grid[(x + diff_x) as usize][(y + diff_y) as usize]
{
x += diff_x;
y += diff_y;
}
}
grid[x as usize][y as usize] = true;
if verbose {
println!("particle {:5>}/{}", particle_i + 1, num_particles);
}
}
grid
}
fn random_cell_placement(
strategy: CellPlacementStrategy,
rng: &ChaCha8Rng,
grid: &Vec<Vec<bool>>,
width: usize,
height: usize,
) -> (isize, isize) {
let mut rng = rng.to_owned();
match strategy {
CellPlacementStrategy::Anywhere => {
let mut x: isize = rng.gen_range(0..width).try_into().unwrap();
let mut y: isize = rng.gen_range(0..height).try_into().unwrap();
while is_stuck((x, y), width, height, &grid) || grid[x as usize][y as usize] {
x = rng.gen_range(0..width).try_into().unwrap();
y = rng.gen_range(0..height).try_into().unwrap();
}
(x, y)
}
CellPlacementStrategy::Corners => {
let possibilities: Vec<(isize, isize)> = vec![
(0, 0),
(0, height as isize - 1),
(width as isize - 1, 0),
(width as isize - 1, height as isize - 1),
];
let possibilities: Vec<&(isize, isize)> = possibilities
.iter()
.filter(|(pos_x, pos_y)| {
!grid
.get(*pos_x as usize)
.unwrap()
.get(*pos_y as usize)
.unwrap()
})
.collect();
if possibilities.is_empty() {
// corners are filled - place anywhere
random_cell_placement(CellPlacementStrategy::Anywhere, &rng, grid, width, height)
} else {
// pick any of the available corners
let index = rng.gen_range(0..possibilities.len());
possibilities.get(index).unwrap().to_owned().to_owned()
}
}
}
}
fn is_stuck(xy: (isize, isize), width: usize, height: usize, grid: &Vec<Vec<bool>>) -> bool {
let (x, y) = xy;
if x - 1 >= 0 && grid[usize::try_from(x - 1).unwrap()][usize::try_from(y).unwrap()] {
return true;
}
if x + 1 < width.try_into().unwrap()
&& grid[usize::try_from(x + 1).unwrap()][usize::try_from(y).unwrap()]
{
return true;
}
if y - 1 >= 0 && grid[usize::try_from(x).unwrap()][usize::try_from(y - 1).unwrap()] {
return true;
}
if y + 1 < height.try_into().unwrap()
&& grid[usize::try_from(x).unwrap()][usize::try_from(y + 1).unwrap()]
{
return true;
}
return false;
}