10
\$\begingroup\$

From Rosetta Code:

The Archimedean spiral is a spiral named after the Greek mathematician Archimedes. It can be described by the equation: $$r=a+b\theta$$ with real numbers \$a\$ and \$b\$.

Here is my attempt to draw it in Python (using Pillow):

"""This module creates an Archimdean Spiral."""

from math import cos, sin, pi
from PIL import Image, ImageDraw


def translate(point, screen_size):
    """
    Takes a point and converts it to the appropriate coordinate system.
    Note that PIL uses upper left as 0, we want the center.
    Args:
        point (real, real): A point in space.
        screen_size (int): Size of an N x N screen.
    Returns:
        (real, real): Translated point for Pillow coordinate system.
    """
    return point[0] + screen_size / 2, point[1] + screen_size / 2


def draw_spiral(a, b, img, step=0.5, loops=5):
    """
    Draw the Archimdean spiral defined by:
    r = a + b*theta
    Args:
        a (real): First parameter
        b (real): Second parameter
        img (Image): Image to write spiral to.
        step (real): How much theta should increment by. (default: 0.5)
        loops (int): How many times theta should loop around. (default: 5)
    """
    draw = ImageDraw.Draw(img)
    theta = 0.0
    r = a
    prev_x = int(r*cos(theta))
    prev_y = int(r*sin(theta))
    while theta < 2 * loops * pi:
        theta += step
        r = a + b*theta
        # Draw pixels, but remember to convert to Cartesian:
        x = int(r*cos(theta))
        y = int(r*sin(theta))
        draw.line(translate((prev_x, prev_y), img.size[0]) +
                  translate((x, y), img.size[0]), fill=1)
        prev_x = x
        prev_y = y


if __name__ == '__main__':
    IMAGE_SIZE = 300, 300
    img = Image.new('1', IMAGE_SIZE)
    draw_spiral(1, 2, img)
    img.save('spiral.png')

The program outputs this image:

\$\endgroup\$
7
\$\begingroup\$

With the default values, the spiral does not look very smooth: it's easy to see that it's been drawn using a series of straight-line segments. That's because the step in \$\theta\$ is quite large — 0.5 radians is more than 28°. Setting step=0.1 and loops=10 is an improvement:

But you can still see deviations from smoothness in various places. The problem here is that the step in \$\theta\$ is constant, but as \$r\$ gets bigger this becomes a bigger step in actual distance. What we would like is a (roughly) constant step in terms of distance along the curve, which we get by replacing

theta += step

with

theta += step / r

This makes step into an approximate distance along the curve in pixels (rather than a change in angle, as in the original code). Now, with step=1 and loops=10, we get this spiral:

\$\endgroup\$
1
\$\begingroup\$

While a transformation to cartesian coordinates is not too complicated, I would still consider encapsulating it in a function:

def polar_to_cartesian(r, theta):
    return int(r*cos(theta)), int(r*sin(theta))

which you can then use in draw_spiral (you can even just use pos instead of (x, y)):

def draw_spiral(a, b, img, step=0.5, loops=5):
    """
    Draw the Archimedean spiral defined by:
    r = a + b*theta
    Args:
        a (real): First parameter
        b (real): Second parameter
        img (Image): Image to write spiral to.
        step (real): How much theta should increment by. (default: 0.5)
        loops (int): How many times theta should loop around. (default: 5)
    """
    draw = ImageDraw.Draw(img)
    theta = 0.0
    r = a
    prev_pos = polar_to_cartesian(r, theta)
    while theta < 2 * loops * pi:
        theta += step
        r = a + b*theta
        # Draw pixels, but remember to convert to Cartesian:
        pos = polar_to_cartesian(r, theta)
        draw.line(translate(prev_pos, img.size[0]) +
                  translate(pos, img.size[0]), fill=1)
        prev_pos = pos
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.