Skip to content

Turtle Graphics 🐢

Explanation & Example

ElkPlot includes a recreation of Logo's turtle graphics. This is intended as a quick and easy way for learners to start making art without having to go too deep on the Python programming language. (Yet)

Getting started is super easy - just make a turtle and give it some instructions. You can name your turtle whatever you like - I decided to name mine Gamera.

import elkplot

def main():
    w, h = elkplot.sizes.LETTER

    # the turtle starts at (0, 0) facing right
    gamera = elkplot.Turtle(use_degrees=True)

    # draw the left eye
    gamera.turn_right(90)
    gamera.forward(2)
    gamera.raise_pen()

    # draw the right eye
    gamera.goto(2, 0)
    gamera.lower_pen()
    gamera.forward(2)
    gamera.raise_pen()

    # draw the mouth
    gamera.goto(-1.5, 2.5)
    gamera.lower_pen()
    gamera.turn_left(45)
    gamera.forward(1.5)
    gamera.turn_left(45)
    gamera.forward(3)
    gamera.turn_left(45)
    gamera.forward(1.5)

    # render the drawing, center it on a letter size sheet, and draw
    d = gamera.drawing() 
    d = elkplot.center(d, w, h)
    elkplot.draw(d, w, h, plot=False)


if __name__ == "__main__":
    main()

Turtle Plot Preivew

Checkpoint!

You can use a checkpoint context to return to your current position and rotation upon exiting the context. The following tree fractal uses checkpoints to return to the base of a branch once finishing drawing that branch.

import elkplot


def tree(
    angle: float, length: float, shrink: float, depth: int, turtle: elkplot.Turtle
) -> None:
    if depth <= 0:
        return
    turtle.forward(length)
    with turtle.checkpoint():
        turtle.turn(-angle)
        tree(angle, shrink * length, shrink, depth - 1, turtle)
    with turtle.checkpoint():
        turtle.turn(angle)
        tree(angle, shrink * length, shrink, depth - 1, turtle)


def main():
    w, h, margin = 8, 8, 0.5
    turtle = elkplot.Turtle(use_degrees=True)
    turtle.turn_left(90)
    tree(20, 1, 0.8, 10, turtle)
    drawing = turtle.drawing()
    drawing = elkplot.scale_to_fit(drawing, w, h, margin)
    elkplot.draw(drawing, w, h, plot=False)


if __name__ == "__main__":
    main()

Turtle Tree Fractal

Full Reference

Turtle

Source code in elkplot/turtle.py
class Turtle:
    def __init__(
        self, x: float = 0, y: float = 0, heading: float = 0, use_degrees=False
    ) -> None:
        """Create a new turtle!

        Args:
            x (float, optional): x-coordinate of the turtle's starting position. Defaults to 0.
            y (float, optional): y-coordinate of the turtle's starting position. Defaults to 0.
            heading (float, optional): Direction the turtle is pointing. Defaults to 0.
            use_degrees (bool, optional): Should angles be given in radians or degrees? Defaults to False.
        """
        self._state = TurtleState(shapely.Point(x, y), heading, True)
        self._stack: list[TurtleState] = []
        self._current_line: list[shapely.Point] = [self.position]
        self._lines: list[shapely.LineString] = []
        self._use_degrees = use_degrees

    @property
    def heading(self) -> float:
        """
        Returns:
            float: Which way is the turtle facing?
        """
        if self._use_degrees:
            return self._state.heading * RAD_TO_DEG
        return self._state.heading

    @property
    def heading_rad(self) -> float:
        return self._state.heading

    @property
    def position(self) -> shapely.Point:
        """
        Returns:
            shapely.Point: Where is the turtle right now?
        """
        return self._state.position

    @property
    def x(self) -> float:
        """
        Returns:
            float: The turtle's x-coordinate
        """
        return self._state.position.x

    @property
    def y(self) -> float:
        """
        Returns:
            float: The turtle's y-coordinate
        """
        return self._state.position.y

    @property
    def pen_down(self) -> bool:
        """
        Returns:
            bool: Is the turtle drawing currently
        """
        return self._state.pen_down

    def forward(self, distance: float) -> Turtle:
        """Move the turtle forward by some distance. You can also move the turtle
          backwards by calling this function with a negative input

        Args:
            distance (float): The distance to move forward

        Returns:
            Turtle: Return self so that commands can be chained
        """
        dx, dy = distance * np.cos(self.heading_rad), distance * np.sin(
            self.heading_rad
        )
        return self.goto(self.x + dx, self.y + dy)

    def backward(self, distance: float) -> Turtle:
        """Move the turtle backward by some distance.

        Args:
            distance (float): The distance to move backward

        Returns:
            Turtle: Return self so that commands can be chained
        """
        return self.forward(-distance)

    def turn(self, angle: float) -> Turtle:
        """Rotate clockwise by some angle. To rotate counterclockwise, pass a negative angle.

        Args:
            angle (float): The angle by which to rotate

        Returns:
            Turtle: Return self so that commands can be chained
        """
        return self.turn_right(angle)

    def turn_right(self, angle: float) -> Turtle:
        """Rotate clockwise by some angle. (This is an alias for turn)

        Args:
            angle (float): The angle by which to rotate

        Returns:
            Turtle: Return self so that commands can be chained
        """
        new_heading = self.heading_rad + angle * (
            DEG_TO_RAD if self._use_degrees else 1
        )
        self._state = dataclasses.replace(self._state, heading=new_heading)
        return self

    def turn_left(self, angle: float) -> Turtle:
        """Rotate anti-clockwise by some angle.

        Args:
            angle (float): The angle by which to rotate

        Returns:
            Turtle: Return self so that commands can be chained
        """
        return self.turn_right(-angle)

    def goto(self, x: float, y: float) -> Turtle:
        """Move the turtle directly to a given coordinate

        Args:
            x (float): The x-coordinate of the point to which to go
            y (float): The y-coordinate of the point to which to go

        Returns:
            Turtle: Return self so that commands can be chained
        """
        new_pos = shapely.Point(x, y)
        if self.pen_down:
            self._current_line.append(new_pos)
        self._state = dataclasses.replace(self._state, position=new_pos)
        return self

    def raise_pen(self) -> Turtle:
        """Lift the pen so that lines are not created when the turtle moves

        Returns:
            Turtle: Return self so that commands can be chained
        """
        if len(self._current_line) > 1:
            self._lines.append(shapely.LineString(self._current_line))
        self._current_line = [self.position]
        self._state = dataclasses.replace(self._state, pen_down=False)
        return self

    def lower_pen(self) -> Turtle:
        """Lower the pen so that lines are created when the turtle moves

        Returns:
            Turtle: Return self so that commands can be chained
        """
        self._current_line = [self.position]
        self._state = dataclasses.replace(self._state, pen_down=True)
        return self

    def push(self) -> Turtle:
        """Push the turtle's current state (position & angle) onto a stack
        so that the turtle can revert to that position later

        Returns:
            Turtle: Return self so that commands can be chained
        """
        self._stack.append(self._state)
        return self

    def pop(self) -> Turtle:
        """Pop the top state from the stack and revert the turtle to that state.
        New lines are not created by the turtle jumping back to this old state.

        Returns:
            Turtle: Return self so that commands can be chained
        """
        self.raise_pen()
        self._state = self._stack.pop()
        if self.pen_down:
            self._current_line = [self.position]
        return self

    def drawing(self) -> shapely.MultiLineString:
        """Turn the paths drawn by the turtle into a shapely geometry so that it
        can be further modified or plotted.

        Returns:
            shapely.MultiLineString: The MultiLineString composed from all the lines the turtle drew.
        """
        if self.pen_down:
            self.raise_pen()
            self.lower_pen()
        return shapely.MultiLineString(self._lines)

    def checkpoint(self) -> TurtleCheckpoint:
        """Allows for pushing and popping as part of a context using the `with` keyword."""
        return TurtleCheckpoint(self)

heading: float property

Returns:

Name Type Description
float float

Which way is the turtle facing?

pen_down: bool property

Returns:

Name Type Description
bool bool

Is the turtle drawing currently

position: shapely.Point property

Returns:

Type Description
Point

shapely.Point: Where is the turtle right now?

x: float property

Returns:

Name Type Description
float float

The turtle's x-coordinate

y: float property

Returns:

Name Type Description
float float

The turtle's y-coordinate

__init__(x=0, y=0, heading=0, use_degrees=False)

Create a new turtle!

Parameters:

Name Type Description Default
x float

x-coordinate of the turtle's starting position. Defaults to 0.

0
y float

y-coordinate of the turtle's starting position. Defaults to 0.

0
heading float

Direction the turtle is pointing. Defaults to 0.

0
use_degrees bool

Should angles be given in radians or degrees? Defaults to False.

False
Source code in elkplot/turtle.py
def __init__(
    self, x: float = 0, y: float = 0, heading: float = 0, use_degrees=False
) -> None:
    """Create a new turtle!

    Args:
        x (float, optional): x-coordinate of the turtle's starting position. Defaults to 0.
        y (float, optional): y-coordinate of the turtle's starting position. Defaults to 0.
        heading (float, optional): Direction the turtle is pointing. Defaults to 0.
        use_degrees (bool, optional): Should angles be given in radians or degrees? Defaults to False.
    """
    self._state = TurtleState(shapely.Point(x, y), heading, True)
    self._stack: list[TurtleState] = []
    self._current_line: list[shapely.Point] = [self.position]
    self._lines: list[shapely.LineString] = []
    self._use_degrees = use_degrees

backward(distance)

Move the turtle backward by some distance.

Parameters:

Name Type Description Default
distance float

The distance to move backward

required

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def backward(self, distance: float) -> Turtle:
    """Move the turtle backward by some distance.

    Args:
        distance (float): The distance to move backward

    Returns:
        Turtle: Return self so that commands can be chained
    """
    return self.forward(-distance)

checkpoint()

Allows for pushing and popping as part of a context using the with keyword.

Source code in elkplot/turtle.py
def checkpoint(self) -> TurtleCheckpoint:
    """Allows for pushing and popping as part of a context using the `with` keyword."""
    return TurtleCheckpoint(self)

drawing()

Turn the paths drawn by the turtle into a shapely geometry so that it can be further modified or plotted.

Returns:

Type Description
MultiLineString

shapely.MultiLineString: The MultiLineString composed from all the lines the turtle drew.

Source code in elkplot/turtle.py
def drawing(self) -> shapely.MultiLineString:
    """Turn the paths drawn by the turtle into a shapely geometry so that it
    can be further modified or plotted.

    Returns:
        shapely.MultiLineString: The MultiLineString composed from all the lines the turtle drew.
    """
    if self.pen_down:
        self.raise_pen()
        self.lower_pen()
    return shapely.MultiLineString(self._lines)

forward(distance)

Move the turtle forward by some distance. You can also move the turtle backwards by calling this function with a negative input

Parameters:

Name Type Description Default
distance float

The distance to move forward

required

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def forward(self, distance: float) -> Turtle:
    """Move the turtle forward by some distance. You can also move the turtle
      backwards by calling this function with a negative input

    Args:
        distance (float): The distance to move forward

    Returns:
        Turtle: Return self so that commands can be chained
    """
    dx, dy = distance * np.cos(self.heading_rad), distance * np.sin(
        self.heading_rad
    )
    return self.goto(self.x + dx, self.y + dy)

goto(x, y)

Move the turtle directly to a given coordinate

Parameters:

Name Type Description Default
x float

The x-coordinate of the point to which to go

required
y float

The y-coordinate of the point to which to go

required

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def goto(self, x: float, y: float) -> Turtle:
    """Move the turtle directly to a given coordinate

    Args:
        x (float): The x-coordinate of the point to which to go
        y (float): The y-coordinate of the point to which to go

    Returns:
        Turtle: Return self so that commands can be chained
    """
    new_pos = shapely.Point(x, y)
    if self.pen_down:
        self._current_line.append(new_pos)
    self._state = dataclasses.replace(self._state, position=new_pos)
    return self

lower_pen()

Lower the pen so that lines are created when the turtle moves

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def lower_pen(self) -> Turtle:
    """Lower the pen so that lines are created when the turtle moves

    Returns:
        Turtle: Return self so that commands can be chained
    """
    self._current_line = [self.position]
    self._state = dataclasses.replace(self._state, pen_down=True)
    return self

pop()

Pop the top state from the stack and revert the turtle to that state. New lines are not created by the turtle jumping back to this old state.

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def pop(self) -> Turtle:
    """Pop the top state from the stack and revert the turtle to that state.
    New lines are not created by the turtle jumping back to this old state.

    Returns:
        Turtle: Return self so that commands can be chained
    """
    self.raise_pen()
    self._state = self._stack.pop()
    if self.pen_down:
        self._current_line = [self.position]
    return self

push()

Push the turtle's current state (position & angle) onto a stack so that the turtle can revert to that position later

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def push(self) -> Turtle:
    """Push the turtle's current state (position & angle) onto a stack
    so that the turtle can revert to that position later

    Returns:
        Turtle: Return self so that commands can be chained
    """
    self._stack.append(self._state)
    return self

raise_pen()

Lift the pen so that lines are not created when the turtle moves

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def raise_pen(self) -> Turtle:
    """Lift the pen so that lines are not created when the turtle moves

    Returns:
        Turtle: Return self so that commands can be chained
    """
    if len(self._current_line) > 1:
        self._lines.append(shapely.LineString(self._current_line))
    self._current_line = [self.position]
    self._state = dataclasses.replace(self._state, pen_down=False)
    return self

turn(angle)

Rotate clockwise by some angle. To rotate counterclockwise, pass a negative angle.

Parameters:

Name Type Description Default
angle float

The angle by which to rotate

required

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def turn(self, angle: float) -> Turtle:
    """Rotate clockwise by some angle. To rotate counterclockwise, pass a negative angle.

    Args:
        angle (float): The angle by which to rotate

    Returns:
        Turtle: Return self so that commands can be chained
    """
    return self.turn_right(angle)

turn_left(angle)

Rotate anti-clockwise by some angle.

Parameters:

Name Type Description Default
angle float

The angle by which to rotate

required

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def turn_left(self, angle: float) -> Turtle:
    """Rotate anti-clockwise by some angle.

    Args:
        angle (float): The angle by which to rotate

    Returns:
        Turtle: Return self so that commands can be chained
    """
    return self.turn_right(-angle)

turn_right(angle)

Rotate clockwise by some angle. (This is an alias for turn)

Parameters:

Name Type Description Default
angle float

The angle by which to rotate

required

Returns:

Name Type Description
Turtle Turtle

Return self so that commands can be chained

Source code in elkplot/turtle.py
def turn_right(self, angle: float) -> Turtle:
    """Rotate clockwise by some angle. (This is an alias for turn)

    Args:
        angle (float): The angle by which to rotate

    Returns:
        Turtle: Return self so that commands can be chained
    """
    new_heading = self.heading_rad + angle * (
        DEG_TO_RAD if self._use_degrees else 1
    )
    self._state = dataclasses.replace(self._state, heading=new_heading)
    return self