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()
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:
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()
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.