Spirograph code in generativepy

Martin McBride
2022-01-26

In this article we will see how to create Spirograph patterns in Python using generativepy.

As we saw in the introductory article, the points on the Spirograph curve are calculated using parametric equations, and the shape is controlled by three values a, b, and c.

The code

Here is the code to create a Spirograph pattern:

from generativepy.color import Color
from generativepy.drawing import make_image, setup
import math

from generativepy.geometry import Polygon, Transform

def create_spiro(a, b, d):
    dt = 0.01
    t = 0
    pts = []
    while t < 2*math.pi*b/math.gcd(a, b):
        t += dt
        x = (a - b) * math.cos(t) + d * math.cos((a - b)/b * t)
        y = (a - b) * math.sin(t) - d * math.sin((a - b)/b * t)
        pts.append((x, y))
    return pts


def draw(ctx, pixel_width, pixel_height, frame_no, frame_count):

    width = 32
    setup(ctx, pixel_width, pixel_height, width=width, startx=-width/2, starty=-width/2, background=Color(1))

    a = 14
    b = 6
    d = 4
    Polygon(ctx).of_points(create_spiro(a, b, d)).stroke(Color('red'), line_width=0.1)


make_image("spirograph.png", draw, 600, 600)

The function create_spiro creates a set of points that lay on the curve, using the parametric equations described in the introductory article. As we saw, the complete Spirograph often requires the curve to be calculated over several rotations. The number of rotations depends on a and b, and is given by b/gcd(a, b). Since each rotation is 2*pi radians, we must calculate points of t in the range zero to:

2*math.pi*b/math.gcd(a, b)

The loop calculates point fir t in increments of 0.01, which creates a smooth curve.

The function returns a list of all the points calculated.

The curve is drawn using the standard make_image function of generativepy. This uses a draw function to do the drawing, in the usual way.

In the draw function, we simply draw a polygon base on the calculated points. As noted above, because the points are so close together, the polygon appears to be a smooth curve.

Here is the result:

The code can be found on github in blog/geometric/spirograph.py.

Variation - multiple plots

We can create more complex patterns by plotting multiple curves on the same image. We can use different a, b, or c numbers for each curve, and also try different colours. There are many possibilities.

The code is fairly easy, we just make multiple calls to the create_spiro function:

def draw2(ctx, pixel_width, pixel_height, frame_no, frame_count):

    width = 32
    setup(ctx, pixel_width, pixel_height, width=width, startx=-width/2, starty=-width/2, background=Color(1))

    a = 16
    b = 13
    d = 5
    Polygon(ctx).of_points(create_spiro(a, b, d)).stroke(Color('firebrick'), line_width=0.1)

    a = 16
    b = 9
    d = 8
    Polygon(ctx).of_points(create_spiro(a, b, d)).stroke(Color('goldenrod'), line_width=0.1)

    a = 16
    b = 11
    d = 6
    Polygon(ctx).of_points(create_spiro(a, b, d)).stroke(Color('darkgreen'), line_width=0.1)


make_image("spirograph2.png", draw2, 600, 600)

Here is the result:

Variation - rotated curves

Another possible variation is to draw the exact same curve multiple times, but rotate it a little each time. In the code below we use a loop to draw the same curve 6 times, rotating it by 0.05 radians (about 3 degrees) each time:

def draw3(ctx, pixel_width, pixel_height, frame_no, frame_count):

    width = 32
    setup(ctx, pixel_width, pixel_height, width=width, startx=-width/2, starty=-width/2, background=Color(1))

    for i in range(6):
        a = 13
        b = 7
        d = 5
        with Transform(ctx).rotate(0.05*i):
            Polygon(ctx).of_points(create_spiro(a, b, d)).stroke(Color('dodgerblue').with_l_factor(1.1**i), line_width=0.1)


make_image("spirograph3.png", draw3, 600, 600)

To add a bit more interest, we also change the colour slightly:

Color('dodgerblue').with_l_factor(1.1**i)

This sets the basic colour to the CSS named colour dodgerblue, but it then adjusts the lightness of the colour by calling with_l_factor. This creates a lighter version of the same colour.

Here is the result: