The Artist's Husband: The City

/2024/06/the-artists-husband-the-city/images/the_city.png

As I was poking around the Internet the other day, I ran across Jared S Tarbell's Gallery of Computation . I was really impressed with what he had done, especially since a few of his ideas were exactly where I was trying to go! I believe I had seen something about his Sand Stroke a few years back, and liked the effect. My Watercolor Stroke is the same basic idea, clearly borrowed from Sand Stroke! Now that I’ve found the site again, I’ll be studying his code closely!

There is an image on that site called "Substrate" which I really like. Here is a portion of one of his variations.

/2024/06/the-artists-husband-the-city/images/substrate.png

I’m wondering if I can do something similar using Hyphae and Watercolor Stroke.

By the way, if you’re trying to remember where you’ve heard of Jared S Tarbell before: he was one of the founders of Etsy.com .

The Hyphae class in TAHGA is the one we talked about last week . I did need to make an additional change to the Hypha class and its factory class. I wanted to have a collection of watercolor strokes to use randomly, each a different color. However, once a stroke of a particular color is chosen for a Hypha, I want to use the same stroke for that Hypha until it dies. Any new ones branching off can be a new randomly chosen stroke. Sadly, there is no way to embed a stroke into a Hypha. Nor should there be! This a is pretty specific need, unlikely to be repeated exactly during future uses of the Hypha class. I needed a more general solution.

What I did was add a single field to the Hypha structure, an integer. The field is called cargo, as it’s just data carried along with the Hypha, it doesn’t really have anything to do with the Hypha itself. Then I added a field to the factory which I can use to pass in a function which while give me a useful value whenever a new Hypha is created. I use the integer as an index into an array of watercolor strokes, so I always know which stroke to use when drawing that particular Hypha. This is a more general solution because the user of the Hypha class has complete control over what that integer means. In this case, I’m using it as in index into an array of strokes. A future user of the class can use it to refer to, say, a level of transparency, or perhaps a line style (dashed or dotted, instead of solid.) Or, if it’s not needed, it cam be ignored entirely. You can see the new field in the documentation . The documentation always describes the latest version of TAHGA, and it may have changed by the time you read this (it’s changing all the time!) If you want the exact version of the library used in this post, check out the tag tah-the-city from git .

Here is the program I came up with:

use std::process;

use nannou::prelude::*;
use tahga::growth::hyphae::hypha::Factory;
use tahga::growth::hyphae::Hyphae;
use tahga::stroke::watercolor::{StrokeType, WatercolorStroke};

const WIDTH: u32 = 600;
const HEIGHT: u32 = 600;
const CAPTURE: bool = false;
const STROKE_COUNT: usize = 5;
const ROOT_COUNT: usize = 20;

fn main() {
    nannou::app(model)
        .update(update)
        .run();
}

struct Model {
    hyphae: Hyphae,
    strokes: Vec<WatercolorStroke>,

    paused: bool,
    ctrl_key_pressed: bool,
}

fn model(app: &App) -> Model {
    app
        .new_window()
        .size(WIDTH, HEIGHT)
        .view(view)
        .key_pressed(key_pressed)
        .key_released(key_released)
        .build()
        .unwrap();

    let factory = Factory::new()
        .fn_direction_variance(|| 0.0)
        .fn_branch_probability(|| 0.1)
        .fn_speed_multiplier(|| 1.)
        .fn_cargo(|| random_range(0, STROKE_COUNT as i32))
        ;
    let mut hyphae = Hyphae::new_with_factory(factory);

    let mut strokes = Vec::new();
    let stroke_load = 50.;
    let saturation = 0.7;
    let lightness = 0.8;
    for i in 0..STROKE_COUNT {
        let stroke = WatercolorStroke::new()
            .color(hsla(i as f32 / STROKE_COUNT as f32, saturation, lightness, 0.0))
            .stroke_type(StrokeType::Left)
            .count(2)
            .load(stroke_load);
        strokes.push(stroke);
    }

    for _i in 0..ROOT_COUNT {
        let position = vec2(
            random_range(-(WIDTH as f32 / 2.0) + 100., WIDTH as f32 / 2.0 - 100.),
            random_range(-(HEIGHT as f32 / 2.0) + 100., HEIGHT as f32 / 2.0 - 100.),
        );
        hyphae.add_hypha(
            hyphae.new_hypha()
                .position(position)
        );
    }

    Model {
        hyphae,
        strokes,

        paused: false,
        ctrl_key_pressed: false,
    }
}

fn update(app: &App, model: &mut Model, _update: Update) {
    if model.paused { return; }

    model.hyphae.update(app);

    if !model.hyphae.living {
        model.paused = true;
        eprintln!("All {} hyphae have died", model.hyphae.hyphae.len());
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    if model.paused { return; }

    let draw = app.draw();
    if app.elapsed_frames() == 1 {
        draw.background().color(WHITE);
    }

    for hypha in &model.hyphae.hyphae {
        if hypha.dead {
            continue;
        }
        match hypha.path.last() {
            Some(line) => {
                model.strokes[hypha.cargo as usize].draw(&draw, line.start, line.end);
                draw.line()
                    .start(line.start)
                    .end(line.end)
                ;
            }
            None => {}
        }
    }
    draw.to_frame(app, &frame).unwrap();

    if CAPTURE {
        let file_path = captured_frame_path(app, &frame);
        app.main_window().capture_frame(file_path);
    }
}

/// React to key-presses
fn key_pressed(app: &App, model: &mut Model, key: Key) {
    match key {
        Key::C => {
            if model.ctrl_key_pressed {
                process::exit(0);
            }
        }
        Key::S => {
            let file_path = saved_image_path(app);
            app.main_window().capture_frame(file_path);
        }
        Key::Space => {
            model.paused = !model.paused;
        }
        Key::LControl => {
            model.ctrl_key_pressed = true;
        }
        _other_key => {}
    }
}

/// React to key releases
fn key_released(_app: &App, model: &mut Model, key: Key) {
    match key {
        Key::LControl => {
            model.ctrl_key_pressed = false;
        }
        _other_key => {}
    }
}

fn captured_frame_path(app: &App, frame: &Frame) -> std::path::PathBuf {
    app.project_path()
        .expect("failed to locate `project_path`")
        .join("frames")
        .join(format!("{:04}", frame.nth()))
        .with_extension("png")
}

/// Get the path to the next saved image
fn saved_image_path(app: &App) -> std::path::PathBuf {
    app.project_path()
        .expect("failed to locate `project_path`")
        .join("saved")
        .join(format!("image{:05}", chrono::offset::Local::now()))
        .with_extension("png")
}

As you can see, the model contains a Hyphae instance, plus an array of WatercolorStroke instances. While building the model, we create a Factory instance which changes a number of default Hypha values. In particular, we set the direction variance to 0 so each line we draw ends up perfectly straight, we greatly reduce the branch probability so there is more space between the lines, and we set the speed multiplier to 1 so that the lines grow at a constant speed. Then we set the cargo function to product a random value between 0 and STROKE_COUNT, which we set to 5 earlier. This means the cargo value will always end up being an integer between 0 and 4, since random_value() will never give us that upper limit of 5, just values less than that.

After creating a Hyphae instance with that factory, we create the array of WatercolorStroke instances. There are STROKE_COUNT (5) strokes, and in the way of arrays, these are indexed as 0 through 4.

Lastly, we create an initial set of Hypha instances – in this case, we set the ROOT_COUNT to 20, creating 20 Hypha instances scattered randomly throughout the canvas.

The next point of interest in this program is the view() method. As you may recall, a Hyphae instance has its own color, which is used for all it’s Hypha instances, and Hypha instances each have a weight field which is used to describe the thickness of the line. This weight decreases every time a new Hypa branches, which is why the lines get thinner and thinner. All of that is still going on in this program through default values, but we’re going to ignore it. Instead. every time we come to the view() method, we’re going to draw the last segment of each Hypha as a watercolor stroke using the instance of WatercolorStroke pointed to by the cargo field. Then we’re going to draw that segment again as a black line.

That’s it! The effect will be black lines with color flowing off of them, with the lines growing until the rules say they die (they would leave the canvas or touch another line.)

Here’s the result, which I will call The City for no reason other than that it reminds me of one:

/2024/06/the-artists-husband-the-city/images/the_city.png

It’s not as complex as Tarbell’s original, nor, because I used the full color wheel, is it near as subtle, The original uses a completely different algorithm to generate the lines; it does not use the Hyphea algorithm at all. It might be better to back off on the color variation and reduce the watercolor stroke load value each time a new Hypha branches off, but this is an interesting start.