Home

Frost Patterns

things to make and do on a train

people also said it looks like randomly generated dungeons or mycelium

I learned about an algorithm called Diffusion-limited aggregation from this youtube video about procedural landscape generation for games.

The process looks like this:

It happens on an empty grid with one cell in the middle. Let's call that cell the shape. Then, in each next step of the algorithm, a new cell is spawned in a random location and does a random walk until it touches a side of the shape, after which it stays there and becomes a part of the shape.

Me infodumping about this to Shebang

cause copying dms is more fun than writing an article

Shebang system
hiiiiii :3
cqql
:3
I was coding a thing
more generative art
I started with this:
an algorithm for generating organic shapes like this
then image generation time
Shebang system
you know how to make conversations with neurodivergent people by immediately infodumping.
congratulations.
this is a fine skill to have
cqql
cqql
Shebang system you know how to make conversations with neurodivergent people by immediately infodumping. congratulations. this is a fine skill to have
yeah!
and now I got scared
because I had to do something to make it look interesting
first I tried blurring
some of them look more interesting than the original
so I decided to try coloring them with gradients
Shebang system
this is very interesting
cqql
this already looks fun! but I think it's still boring, so it needs more.
too much bouba, needs more kiki
Shebang system
REAL
it needs pointy grrrrr trrrnsgenderism
cqql
I decided to try overlaying the original unblurred shape in a different color that matches the scheme
and here we are
Shebang system
omfg
cqql

we ended up generating the account header for shebang's fedi account with this.

Implementation

I published the code in this git repo: frost_patterns.

It builds into an executable with a cli interface for easy image generation. All information about it is in the README.md file.

Minimal example

I wrote it in Rust, here is the DLA algorithm for generating the grid:

fn generate_grid() -> [[bool; HEIGHT]; WIDTH] {
    let mut rng = ChaCha8Rng::seed_from_u64(SEED);

    let mut grid: [[bool; HEIGHT]; WIDTH] = [[false; HEIGHT]; WIDTH];

    grid[WIDTH / 2][HEIGHT / 2] = true;

    for particle_i in 0..NUM_PARTICLES {
        // place the particle randomly
        let mut x: isize = rng.gen_range(0..WIDTH).try_into().unwrap();
        let mut y: isize = rng.gen_range(0..HEIGHT).try_into().unwrap();

        // replace it randomly until it's on an empty space
        while is_stuck((x, y), &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();
        }

        // random walk until it attaches to the main shape
        while !is_stuck((x, y), &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;
            }
        }

        // make it part of the main shape
        grid[x as usize][y as usize] = true;

        // the generation takes a while usually, it's 
        // a good idea to have an idea of the progress
        println!("particle {:5>}/{}", particle_i + 1, NUM_PARTICLES);
    }

    grid
}

// checks if any neighbors of the cell are part of the shape
fn is_stuck(xy: (isize, isize), grid: &[[bool; HEIGHT]; WIDTH]) -> 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;
}

You can start using this code just by printing out the grid like how I did at the start.

fn print_grid(grid: [[bool; HEIGHT]; WIDTH]) {
    for y in 0..HEIGHT {
        for x in 0..WIDTH {
            match grid[x][y] {
                true => print!("X"),
                false => print!("."),
            }
        }
        println!();
    }
}

Home