The Artist's Husband: Turtle Graphics

/2024/02/the-artists-husband-turtle-graphics/images/top_image.png

Turtle graphics are a form of vector graphics. In its most basic form, there is an object that moves around the canvas, leaving a trail as it goes. It’s called turtle graphics because you can think of the object as turtle with a pen strapped to her shell. The turtle can do two things: 1) move in a straight line for a certain distance, and 2) rotate to face a new direction. With a combination of move and rotate commands, the turtle can draw many kinds of objects.

I’ve implemented a basic Turtle structure in Rust:

use nannou::Draw;
use nannou::prelude::*;

pub struct Turtle {
    position: Vector2,
    direction: f32,
    color: Rgb8,
}

impl Turtle {
    pub fn new() -> Self {
        Self {
            position: vec2(0., 0.),
            direction: 0.,
            color: WHITE,
        }
    }

    pub fn position(mut self, position: Vector2) -> Self {
        self.position = position;

        self
    }

    pub fn direction(mut self, direction: f32) -> Self {
        self.direction = direction;

        self
    }

    pub fn color(mut self, color: Rgb8) -> Self {
        self.color = color;

        self
    }

    pub fn draw(&mut self, draw: &Draw, distance: f32) {
        let position = self.position + vec2(
            distance * self.direction.sin(),
            distance * self.direction.cos(),
        );
        draw.line()
            .color(self.color)
            .start(self.position)
            .end(position);
        self.position = position;
    }

    pub fn rotate(&mut self, radians: f32) {
        self.direction += radians;
    }

    pub fn rotate_deg(&mut self, degrees: f32) {
        self.rotate(degrees * std::f32::consts::PI / 180.)
    }
}

This isn’t a full program, just a rust library defining the Turtle class. As you can see, Turtle holds several bits of information: Its position and the direction it is facing. I have also added a field for the color of its pen.

The rest of the class definition are methods that can be used to affect the turtle. There are some builder methods: new(), which creates a new turtle with all its fields set to default values, and position(), direction() and color(), which will change the values of the fields. Note that these return the turtle after it is modified, so they can be chained together in a builder pattern. For example, this creates a new turtle:

    let turtle = Turtle::new();

This creates a new turtle just like in the last example, but with the turtle starting at (10, 10) and carrying a red pen:

    let turtle = Turtle::new().position(vec2(10., 10.)).color(RED);

There is a draw() method which draws a line from the turtle’s current position some distance in the direction in which the turtle is facing. It also updates the turtle’s position to the new location.

There are two (count ’em, TWO) rotate methods: rotate() and rotate_deg(). The first rotates by the given number of radians, and the second is a helper method that lets you use degrees instead of radians.

This is a very basic implementation. A turtle could hold more useful information, such as the thickness of the pen, the type of line the pen will draw (perhaps a choice between dashed and solid,) a flag defining whether the pen is touching the canvas so we can move the turtle without drawing a line, etc. But with just what we have here, we can do a lot.

Here is a Nannou sketch which draws a spiral by moving a distance, rotating 45 degrees, and moving again. After every 4 lines, it increases the distance it will move.

use nannou::prelude::*;
use turtle::Turtle;

fn main() {
    nannou::sketch(view).run();
}

fn view(app: &App, frame: Frame) {
    let mut turtle = Turtle::new().color(RED);

    let draw = app.draw();

    for x in 1..30 {
        let distance = x as f32 * 10.;
        for _i in 0..4 {
            turtle.draw(&draw, distance);
            turtle.rotate_deg(45.);
        }
    }

    draw.to_frame(app, &frame).unwrap();
}

This is what it draws:

/2024/02/the-artists-husband-turtle-graphics/images/spiral.png

Here is another example:

use nannou::prelude::*;
use turtle::Turtle;

fn main() {
    nannou::sketch(view).run();
}

fn view(app: &App, frame: Frame) {
    let mut turtle = Turtle::new().color(YELLOW).position(vec2(0., -200.));

    let draw = app.draw();

    let distance = 500.;
    for x in 1..46 {
        turtle.draw(&draw, distance);
        turtle.rotate_deg(176.);
    }

    draw.to_frame(app, &frame).unwrap();
}
/2024/02/the-artists-husband-turtle-graphics/images/star.png

Note that these are Nannou sketches, not Nannou apps. In a sketch, there is no program state between views. There is only a view() function that draws the same image over and over again. The apps we used in recent posts, for example in the Streams post last week, are designed to run forever so we can see what happens to them over time and grab a screen shot when they look interesting. In a sketch, you need to provide an ending condition so Nannou will eventually draw something on the canvas!

This has been a pretty basic example of turtle graphics; next week we’ll see some more interesting uses for it!