8
\$\begingroup\$

I started coding a week ago. I played a bit with the turtle module and I am looking for ways to improve my code. Please feel free to share improvements.

Here is the shape it creates:

star

#I created a mathematical envelope by connecting points laying on straight lines forming with the x-axis angles of 60, 90, 120, 180... deegrees

import turtle as t
import math

t.speed(0)
t.bgcolor("black")
t.pencolor("deepskyblue")


def star1(start, end, step, x_direction1, y_direction1, x_direction2):  #goes from the x-axis to a 60° straight line (slope = +- 3/sqrt(3))
    for i in range(start, end, step):
        t.penup()
        t.goto(x_direction2 * (255 - 5.1 * i), 0)
        t.pendown()
        t.goto(x_direction1*255/100*(i+1), y_direction1* 3/math.sqrt(3)*255/100 * (i+1))


def star2 (start, end, step, x_direction1, y_direction1, x_direction2, y_direction2): #from 60° straight line to the simmetric in respect to the y-axis
    for i in range (start, end, step):
        t.penup()
        t.goto(x_direction1*(127.5-255/100*i),y_direction1*3/math.sqrt(3)*(127.5-255/100*i))
        t.pendown()
        t.goto(x_direction2*255/100* (i+1), y_direction2 * 3/math.sqrt(3)*255/100*(i+1))


def refine (x_direction, y_direction):   #closing the inner spaces
    t.goto(x_direction * 127.5, y_direction * 3/math.sqrt(3) * 127.5)


def whole_star():      #star with spaces. The start, end and step indexes aren't all the same just for a smoother drawing animation
    star1(0, 50, 1, 1, 1, 1)
    star2(0, 50, 1, 1, 1, -1, 1)
    star1(49, -1, -1, -1, 1, -1)
    star1(0, 50, 1, -1, -1, -1)
    star2(0, 50, 1, -1, -1, 1, -1)
    star1(49, -1, -1, 1, -1, 1)


def final_star():      #refined star
    whole_star()
    t.penup()
    t.goto(255, 0)
    t.pendown()
    t.goto(-255, 0)
    t.penup()
    refine(-1, 1)
    t.pendown()
    refine(1, -1)
    t.penup()
    refine(1, 1)
    t.pendown()
    refine(-1, -1)


final_star()
t.hideturtle()
t.done() 
\$\endgroup\$
0

2 Answers 2

7
\$\begingroup\$

It's good that you got this working (especially since you only started a week ago). It's also unsurprising that you're using turtle, which is geared toward beginner graphics.

As you've already seen, it takes some time for the drawing to render. A more scalable approach is to use Numpy and Matplotlib, which offers several advantages:

  • It's much faster (especially if you write the curve construction to be vectorised)
  • It does polar coordinate systems for you, so that your own curve definitions are much simpler
  • It can draw axes, etc.
  • It's more suitable for "mathematical objects" (however you interpret that)

An implementation could look like:

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.collections import LineCollection


def star(points: int, resolution: int) -> np.ndarray:
    # four-dimensional array: points, resolution, line start/end, angle/radius
    coords = np.empty((points, resolution, 2, 2))

    # all angles in radians
    circle = 2*np.pi
    theta = circle / points   # point angle increment

    # indexer to convert vectors to (n, 1)
    d2 = (slice(None), np.newaxis)

    # start ray angles, including 0, excluding full rotation
    coords[..., 0, 0] = np.arange(
        0, circle - 0.5*theta, theta,
    )[d2]

    # end ray angles, excluding 0, including full rotation
    coords[..., 1, 0] = np.arange(  # end ray angles
        theta, circle + 0.5*theta, theta,
    )[d2]

    # start line radii, starting at the ray centre, excluding the circumference
    coords[..., 0, 1] = np.arange(
        0, 1, 1/resolution,
    )

    # end line radii, starting at the ray circumference, excluding the centre
    coords[..., 1, 1] = np.arange(
        1, 0, -1/resolution,
    )

    # all coords, line start/end, angle/radius
    return coords.reshape((-1, 2, 2))


def plot(coords: np.ndarray) -> plt.Figure:
    plt.style.use('dark_background')
    sky_blue = '#00a0da'
    lines = LineCollection(coords, colors=sky_blue, alpha=0.2)
    fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
    ax.add_collection(lines)
    return fig


def demo() -> None:
    coords = star(points=6, resolution=50)
    plot(coords)
    plt.show()


if __name__ == '__main__':
    demo()

six points

It's also effectively "instant" to generate a much larger diagram, shown here with 16 points and a quite-excessive resolution of 300 line segments per point:

16 points

To get fancier, you can auto-estimate an appropriate line alpha, and auto-assign tick frequency:

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.collections import LineCollection
from matplotlib.ticker import FixedLocator

# ...

def plot(coords: np.ndarray, points: int, resolution: int) -> plt.Figure:
    plt.style.use('dark_background')

    # More resolution means we need more transparent lines
    alpha_estimate = min(1., 10/resolution)
    sky_blue = '#00a0da'
    lines = LineCollection(coords, colors=sky_blue, alpha=alpha_estimate)

    fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
    ax.add_collection(lines)

    # Align the theta axis ticks to evenly spaced points
    circle = 2*np.pi
    theta = circle / points
    locator = FixedLocator(
        locs=np.arange(0, circle - 0.5*theta, theta),
        nbins=12,  # limit tick count
    )
    ax.xaxis.set_major_locator(locator)

    return fig


def demo() -> None:
    points = 6
    resolution = 100
    coords = star(points=points, resolution=resolution)
    plot(coords, points=points, resolution=resolution)
    plt.show()

autoticks

I was wondering if you could also animate it like in turtle.

Yes; matplotlib has animation support:

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.animation import FuncAnimation
from matplotlib.collections import LineCollection
from matplotlib.ticker import FixedLocator


def star(points: int, resolution: int) -> np.ndarray:
    # four-dimensional array: points, resolution, line start/end, angle/radius
    coords = np.empty((points, resolution, 2, 2))

    # all angles in radians
    circle = 2*np.pi
    theta = circle / points   # point angle increment

    # indexer to convert vectors to (n, 1)
    d2 = (slice(None), np.newaxis)

    # start ray angles, including 0, excluding full rotation
    coords[..., 0, 0] = np.arange(
        0, circle - 0.5*theta, theta,
    )[d2]

    # end ray angles, excluding 0, including full rotation
    coords[..., 1, 0] = np.arange(
        theta, circle + 0.5*theta, theta,
    )[d2]

    # start line radii, starting at the ray centre, excluding the circumference
    coords[..., 0, 1] = np.arange(0, 1, 1/resolution)

    # end line radii, starting at the ray circumference, excluding the centre
    coords[..., 1, 1] = np.arange(1, 0, -1/resolution)

    return coords


def make_artists(points: int, resolution: int) -> tuple[
    plt.Figure, plt.PolarAxes, LineCollection,
]:
    plt.style.use('dark_background')

    # More resolution means we need more transparent lines
    alpha_estimate = min(1., 20/resolution)
    sky_blue = '#0ae'
    lines = LineCollection([], colors=sky_blue, alpha=alpha_estimate)

    fig, ax = plt.subplots(subplot_kw={'projection': 'polar'})
    ax.add_collection(lines)

    # Align the theta axis ticks to evenly spaced points
    circle = 2*np.pi
    theta = circle / points
    locator = FixedLocator(
        locs=np.arange(0, circle - 0.5*theta, theta),
        nbins=12,  # limit tick count
    )
    ax.xaxis.set_major_locator(locator)

    return fig, ax, lines


def make_plot(coords: np.ndarray, lines: LineCollection) -> None:
    lines.set_segments(coords.reshape((-1, 2, 2)))


def animate(
    frame: int, coords: np.ndarray, lines: LineCollection,
) -> tuple[plt.Artist, ...]:
    subarray = coords[:, :frame, ...]
    lines.set_segments(subarray.reshape((-1, 2, 2)))
    return lines,


def make_animation(
    fig: plt.Figure, coords: np.ndarray, lines: LineCollection,
    resolution: int, duration: float = 5,
) -> FuncAnimation:
    n_frames = resolution
    frame_ms = 1e3 * duration / n_frames
    return FuncAnimation(
        fig=fig, func=animate, fargs=(coords, lines),
        frames=n_frames, interval=frame_ms, repeat=False,
    )


def demo() -> None:
    points = 6
    resolution = 100
    coords = star(points=points, resolution=resolution)
    fig, ax, lines = make_artists(points=points, resolution=resolution)

    # make_plot(coords=coords, lines=lines)
    anim = make_animation(
        fig=fig, coords=coords, lines=lines, resolution=resolution,
    )
    plt.show()


if __name__ == '__main__':
    demo()

It works, though I can't be bothered to make a screen recording.

\$\endgroup\$
2
  • \$\begingroup\$ First, thank you for all the suggestions. I am really impressed: yours looks way cleaner. I honestly can't understand a single line of your code since i have never tried matplotlib, but I will for sure! I was wondering if you could also animate it like in turtle. \$\endgroup\$
    – Lorenzo
    Commented Mar 12 at 13:54
  • \$\begingroup\$ @Lorenzo yes - see edit \$\endgroup\$
    – Reinderien
    Commented Mar 13 at 1:54
7
\$\begingroup\$

Overview

You've done an excellent job:

  • Overall code layout is good
  • You did a good job partitioning code into functions
  • You leveraged code written by others with the imports
  • Used meaningful names for functions

Here are some adjustments for you to consider, mainly for coding style.

Documentation

Add comments at the top of the file to state the purpose of the code. For example:

'''
I created a mathematical envelope by connecting points laying
on straight lines forming with the x-axis angles of 
60, 90, 120, 180... degrees
'''

Refer to the PEP 8 Style Guide for Comments and Docstrings for more information.

Layout

I recommend moving the functions to the top, after the import statements. Having them in the middle of the code interrupts the natural flow of the code (from a human readability standpoint).

Lint check

pylint identified a few style issues.

It identifies long lines of code and missing function docstrings. Kill two birds with one stone and place those end-of-line comments into docstrings:

   def whole_star():
       '''
       star with spaces. The start, end and step indexes
       aren't all the same just for a smoother drawing animation
       '''

It would be helpful to users of your code to know what value ranges are valid for your function inputs. For example, for the refine function, add the expected ranges for x_direction, etc, to the docstring.
You could even add code to check for valid input values.

Typos

deegrees should be degrees.

simmetric should be symmetric

\$\endgroup\$
0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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