The Artist's Husband: The Secret Life of L-Systems

/2024/03/the-artists-husband-the-secret-life-of-l-systems/images/top_image.png

L-Systems can be used to draw plants! To make this work, we need to add some capabilities to our Turtle structure. Up until now turtles could either move ahead or turn. To draw plants, we need to be able to save a turtle state, do some drawing, and then move the turtle back to the saved state. This has the effect of magically teleporting the turtle to someplace it has already been, restoring its position, direction and color.

We’ll do this with a stack, which is like a list you can put things on the end of, and then take them off. You can save the state as many times as you like, but you can only get them back in the reverse order that you saved them. Think of it like a stack of plates in a cafeteria. Clean plates get added to the top, and when someone wants a plate, they take one from the top. The one they take is the last one that was added.

Our Turtle now looks like this:

pub struct Turtle {
    position: Vec2,
    direction: f32,
    color: Alpha<Hsl, f32>,
    stack: Vec<(Vec2, f32, Alpha<Hsl, f32>)>,
}

In addition to position, direction and color, we now have stack, which is a list of groups of position, direction and color types. There are two new methods for Turtle: push() and pop().

    pub fn push(&mut self) {
        self.stack.push((self.position, self.direction, self.color));
    }

    pub fn pop(&mut self) {
        match self.stack.pop() {
            Some(state) => {
                let (position, direction, color) = state;
                self.position = position;
                self.direction = direction;
                self.color = color;
            }
            None => {}
        }
    }

The push() method saves the current state of the turtle on the stack, and the pop() method restores the state of the turtle to the last time it was saved. It’s important to note that pop() also removes the value it restored, so the next time pop() is called, it will get the next newest previously saved state.

The Nannou app that draws the plant is almost the same as the one from last week's post about L-Systems. The rules are different, and the interpretation of the symbols is different. The important changes are in the model() and view() functions:

fn model(_app: &App) -> Model {
    let axiom = String::from("G");
    let rules = [
        ('F', "FF"),
        ('G', "F+[[G]-G]-F[-FG]+G")
    ];
    Model {
        system: LSystem::new(axiom, &rules),
    }
}

fn view(app: &App, model: &Model, frame: Frame) {
    let mut turtle = Turtle::new()
        .direction_deg(25.)
        .position(vec2(45. - WIDTH as f32 / 2., 45. - HEIGHT as f32 / 2.))
        .color(hsla(0.3, 1., 0.5, 0.25));
    let draw = app.draw();

    let t = app.elapsed_frames() as usize;
    if t >= 4 && t <= 6 {
        for symbol in model.system.builder(t as usize) {
            match symbol {
                'F' => turtle.draw(&draw, 5.),
                '+' => turtle.rotate_deg(30.),
                '-' => turtle.rotate_deg(-20.),
                '[' => turtle.push(),
                ']' => turtle.pop(),
                _ => {}
            }
        }
    }
    draw.to_frame(app, &frame).unwrap()
}
F Move forward by some amount
+ Turn right 30 degrees
- Turn left 20 degrees
[ Save the current state on the stack
] Restore the laste st=aved state from the stack

Note that there is a G symbol, but it doesn’t translate to any action. It is used in the rules though, and it’s important to help generate the structure we are after.

Running the app produces the picture at the top of this post. You can see the full app at https://gitlab.com/theartistshusband/l-systems/-/blob/main/grass/src/main.rs .