Create Galactic Art with Tkinter

One of the wonders of our world is that it can be described with math. The connection is so strong that MIT physicist Max Tegmark believes that the universe isn't just described by math, but that it is math in the sense that we're all parts of a giant mathematical object [1].
What this means is that many seemingly complex objects – across mind-boggling scales – can be reduced to simple equations. Why does a hurricane look like a galaxy? Why is the pattern in a nautilus shell repeated in a pinecone? The answer is math.

Besides their appearance, the objects pictured above have something in common: they all grow, and growth in nature is a geometric progression. Spirals that increase geometrically are considered to be logarithmic, due to the use of the base of the natural logarithm (e) in the equation that describes them. While generally known as logarithmic spirals, their ubiquity in nature has earned them an additional title: spira mirabilis – "miraculous spiral."
In this Quick Success Data Science project, we'll use logarithmic spirals and Python's Tkinter GUI module to simulate a Spiral Galaxy. In the process, we'll generate some attractive and unique digital art.
The Polar Equation for a Logarithmic Spiral
Modeling a spiral galaxy is all about modeling spiral arms. Each spiral arm can be approximated by a logarithmic spiral.

Because spirals radiate out from a central point or pole, you'll more easily graph them with polar coordinates. In this system, the (x, y) coordinates used in the more familiar Cartesian coordinate system are replaced by (r, Ɵ), where r is the distance from the center and Ɵ (theta) is the angle made by r and the x-axis. The coordinates for the pole are (0, 0).

The polar equation for a logarithmic spiral is:

where r is the radius (distance from the origin), Ɵ is the angle measured counterclockwise from the x-axis, e is the base of natural logarithms, a is a scaling factor that controls the size, and b is a growth factor that controls the spiral's "openness" and direction of growth.
The Programming Strategy
To model a four-armed spiral galaxy, we can use the previous formula to draw a single spiral and then rotate and redraw the spiral three more times. We'll build the spirals out of various-sized markers, which will represent stars. While these will be way out of scale for single stars, they will capture the overall brightness patterns in the galaxy (see the title image).
To capture the "background glow" of the galaxy, we'll randomly distribute small "stars" around the galactic disc.
The Tkinter Library
We'll draw our galaxy simulation with Tkinter
(pronounced "tee-kay-inter"). This is the default GUI library for developing desktop applications in Python. Although primarily designed for GUI elements such as windows, buttons, and scroll bars, tkinter
can also generate graphs, screensavers, simple games, and more.
As part of the standard Python distribution, tkinter
is portable across all operating systems and there‘s no need to install external libraries. You can find the official online documentation here.
The Code
The following code was written in the Spyder IDE. You can download the full script from this Gist.
Importing Libraries and Assigning Constants
All of the libraries we need to use are part of the Python Standard Library, so there's no need to install anything. Besides tkinter
, we'll need the math
module for working with equations and random
for randomizing the star locations. The latter will make the simulations stochastic, so each will be slightly different and unique, in case you want to bust out some NFTs.
import math
import tkinter as tk
from random import randint, uniform, random
# Set the scaling factor for the galaxy:
SCALE = 225
The SCALE
constant will change the size of our galaxy. Large numbers will increase the disc diameter and small numbers will reduce it.
Setting Up the tkinter Display Canvas
The following code instantiates a tkinter
window with a canvas on which you can draw things. This is where the galaxy simulation will appear.
# set-up tkinter display canvas:
root = tk.Tk()
root.title("A Spiral Galaxy")
c = tk.Canvas(root, width=1000, height=800, bg='black')
c.grid()
c.configure(scrollregion=(-500, -400, 500, 400))
We start by creating a window with the conventional name root
. This is a top-level window that holds everything else. After naming the window, we add a widget ("Windows gadget") __ to the root window. This Canvas
widget, assigned to the variable c
, is a general-purpose widget intended for graphics and other complex layouts, and it will contain all our drawing objects.
Next, we call the grid()
geometry manager on c
and finish by configuring the canvas to use a scrollregion
. This code sets the origin coordinates (0, 0) to the center of the canvas by using half of the width and height dimensions. We need this setup to draw the galaxy's spiral arms with polar coordinates. Without it, the default origin would be the top-left corner of the canvas.
Defining a Function to Draw a Spiral Arm
Next, we'll define a function to draw a single spiral arm using the logarithmic spiral equation. This spiral may be miraculous, but a large part of the magic is tinkering with the initial bare-bones spiral to "flesh out" the arm. We'll accomplish this by varying the size and location of stars, and by duplicating the spiral for each arm and shifting it slightly backwards while dimming its stars. This creates "leading" and "trailing" edges.
def build_spiral(b, r, rot_fac, fuz_fac, arm):
"""Build a spiral arm for tkinter display with Logarithmic spiral formula.
b = growth factor: negative = spiral to left; larger = more open
r = radius
rot_fac = rotation factor for spiral arm
fuz_fac = fuzz factor to randomly shift star positions
arm = spiral arm (0 = leading edge, 1 = trailing stars)
"""
spiral_stars = []
fuzz = int(0.030 * abs(r)) # Scalable initial amount to shift locations
for i in range(0, 800, 2): # Use range(520) for central "hole"
theta = math.radians(-i)
x = r * math.exp(b*theta) * math.cos(theta - math.pi * rot_fac)
- randint(-fuzz, fuzz) * fuz_fac
y = r * math.exp(b*theta) * math.sin(theta - math.pi * rot_fac)
- randint(-fuzz, fuzz) * fuz_fac
spiral_stars.append((x, y))
for x, y in spiral_stars:
if arm == 0 and int(x % 2) == 0:
c.create_text(x, y,
fill='white',
font=('Helvetica', '6'),
text='*')
elif arm == 0 and int(x % 2) != 0:
c.create_text(x, y,
fill='white',
font=('Helvetica', '5'),
text='*')
elif arm == 1:
c.create_text(x, y,
fill='white',
font=('Helvetica', '5'),
text='.')
Here, we define a function with five parameters. The first two, b
and r
, are from the spiral equation. The next, rot_fac
, is the rotation factor that lets you move the spiral around the center point so you can produce a new spiral arm in a different location.
The fuzz factor, fuz_fac
, lets you tweak how far you move stars away from the center of the spiraling line. Finally, the arm
parameter lets you specify either the leading arm or the trailing arm of faint stars. The trailing arm will be shifted – that is, plotted a little behind the leading arm – and its stars will be smaller (dimmer).
To start the function, we initialize an empty list, called spiral_stars
, to hold the star coordinates. We next assign a fuzz
variable to an arbitrary constant multiplied by the absolute value of the r
(radius) value. This represents a starting point for shifting the stars, which we'll modify later with the fuzz factor (fuz_fac
). It also ensures that the amount of displacement is scalable with respect to the size of the display.
The logarithmic spiral equation alone produces stars that are lined up, as in the left two panels in the figure below. "Fuzzing" moves stars back and forth a little, to either side of the spiral line. You can see the effect on the bright stars in the rightmost panel of the figure.

The next step is to build the spiral lines. We use a range of values to represent Ɵ in the equation. You can tinker with these values to produce different results (this is art, after all).
Next, we loop through the Ɵ values and apply the logarithmic spiral equation, adding the randomized fuzz
value, multiplied by the fuzz_factor
, to the result. We finish the loop by appending the coordinates to the list.
Later, we'll call this function multiple times and specify the rotation factor (rot_fac
) variable, which will move the spiral around the center. After the program builds the four main arms, it will use rot_fac
to build four new arms, slightly offset from the first four, to produce bands of dim, trailing stars. You can see these stars in the rightmost panel of the previous figure; they're the arc of dim stars to the left of each arc of bright stars.
After building the list of star locations, we loop through it, using a conditional statement to choose leading and trailing arms. While tkinter
has the ability to draw circles (with the create_oval()
method), I find that punctuation marks work better at tiny scales. So, we used the create_text()
method instead.
As mentioned earlier, these star objects are for visual impact only. Neither their size nor number is to scale. To be realistic, they would be much, much smaller, and much more numerous (over 100 billion!).
Scattering Star Haze
The space between the spiral arms isn't devoid of stars, so we need a function to randomly cast points across the whole galactic model. Think of these as the "haze" or "glow" you see in photographs of galaxies.
First, we'll need a function to generate random polar coordinates.
def random_polar_coordinates(scale_factor):
"""Generate uniform random x,y point within a 2-D disc."""
n = random()
theta = uniform(0, 2 * math.pi)
x = round(math.sqrt(n) * math.cos(theta) * scale_factor)
y = round(math.sqrt(n) * math.sin(theta) * scale_factor)
return x, y
This function takes our previously defined SCALE
constant as its only argument. It then chooses a float value between 0.0 and 1.0 and assigns it to a variable named n
.
Next, it randomly chooses theta
from a uniform distribution between 0 and 360 degrees (2π is the radian equivalent of 360 degrees).
Finally, it calculates the x and y values over a unit disc, yielding values between -1 and 1, and then multiplies these by the scale factor to scale the results to the size of our galaxy model.
The next function draws the haze stars on the display. It takes as arguments our scale factor constant and an integer multiplier (density
) used to increase or decrease the base number of random stars. So, if you prefer a thick fog rather than a light haze, increase the value of the density
argument.
def star_haze(scale_factor, density):
"""Randomly distribute faint stars in the galactic disc.
SCALE = scaled galactic disc radius
density = multiplier to vary the number of stars posted
"""
for _ in range(0, scale_factor * density):
x, y = random_polar_coordinates(scale_factor)
c.create_text(x, y,
fill='white',
font=('Helvetica', '3'),
text='.')
To plot the stars, we use the same tkinter
method, create_text()
, that we used for the spiral arms. Here's the result with and without star haze.

You can get creative with the haze. For example, increase the density and color them gray, or use a loop to vary both their size and color. Don't use green, however, as there are no green stars in the universe!
Defining a Function to Build the Display
We'll now define a function to build the four main spiral arms and distribute the star haze. Each spiral arm will consist of two calls to our previously defined build_spiral ()
function: one for the leading edge with bright stars (arm=0
) and one for the trailing edge with dimmer stars (arm=1
).
Feel free to play with the parameters. I would recommend changing them one at a time to best judge their impact.
def build_galaxy():
"""Generate the galaxy display with tkinter."""
b=0.3
fuz_fac=1.5
# Build leading and trailing spiral arms:
build_spiral(b=b, r=SCALE, rot_fac=2, fuz_fac=fuz_fac, arm=0)
build_spiral(b=b, r=SCALE, rot_fac=1.91, fuz_fac=fuz_fac, arm=1)
build_spiral(b=b, r=-SCALE, rot_fac=2, fuz_fac=fuz_fac, arm=0)
build_spiral(b=b, r=-SCALE, rot_fac=-2.09, fuz_fac=fuz_fac, arm=1)
build_spiral(b=b, r=-SCALE, rot_fac=0.5, fuz_fac=fuz_fac, arm=0)
build_spiral(b=b, r=-SCALE, rot_fac=0.4, fuz_fac=fuz_fac, arm=1)
build_spiral(b=b, r=-SCALE, rot_fac=-0.5, fuz_fac=fuz_fac, arm=0)
build_spiral(b=b, r=-SCALE, rot_fac=-0.6, fuz_fac=fuz_fac, arm=1)
# Distribute star haze:
star_haze(SCALE, density=30)
# run tkinter loop:
root.mainloop()
build_galaxy()

After calling the build_galaxy()
function, an external window should pop up (or appear as an icon on your taskbar). You'll need to close this window before calling the function again.
As mentioned previously, each display will be absolutely unique. To add depth and interest, try changing half the stars in the leading (arm=0
) arm to light blue or a similar color.

Don't Limit Yourself to Logarithmic Spirals
From the standpoint of digital art, there are more ways to build galaxy-like objects than logarithmic spirals, and more things you can do with the models. Here are some examples from my book, Impractical Python Projects [3].
A Galaxy Far, Far Away
The following model was inspired by Alexandre Devert's post on Marmakoide's Blog, "Spreading Points on a Disc and on a Sphere" [4].

To Boldly Go
The radius of the Milky Way galaxy is roughly 50,000 light-years. When our scale factor (SCALE
) is set to 200, each pixel represents about 250 light-years (50,000 / 200). Knowing this, we can visualize how much of the galaxy the Star Trek Federation could have explored in its first 100 years, assuming they averaged 100x light speed at warp 4:

For this simulation, I used a scale factor of 200 and Earth coordinates of (130, 80).
Immeasurable Heaven
Our radio transmissions currently form an expanding sphere around Earth with a diameter of around 230 light-years. That's basically the size of one of the smallest stars in the previous image. Earth is, as Carl Sagan described it, "just a mote of dust, suspended in a sunbeam."
When you consider that, you can begin to appreciate the sheer enormity and emptiness of our galaxy. Astronomers even have a word for this: Laniakea, Hawaiian for "immeasurable heaven."
Summary
With Python, tkinter
, and a simple equation, we turned a model of a spiral galaxy into some striking digital art that helped us envision our place in the universe. This model has multiple tuning parameters and a stochastic basis, letting you create beautiful and unique works of art.
References
- Tegmark, Max, 2014, "Is the Universe Made of Math?" Scientific American, https://www.scientificamerican.com/article/is-the-universe-made-of-math-excerpt/.
- Vaughan, Lee, 2023, Python Tools for Scientists: An Introduction to Using Anaconda, JuptyerLab, and Python's Scientific Libraries, No Starch Press, San Francisco.
- Vaughan, Lee, 2018, Impractical Python Projects: Playful Programming Activities to Make You Smarter, No Starch Press, San Fransisco.
- Devert, Alexandre, 2012, Spreading Points on a Disc and on a Sphere," Marmakoide's Blog, http://blog.marmakoide.org/?m=201204.
Thanks!
Thanks for reading and please follow me for more Quick Success Data Science projects in the future.