I can't say I would enjoy pen plotting without algorithmic art. But, I also wouldn't enjoy algorithmic art without pen plotting.
The synergy between the two is where the magic happens. It's fun to move pixels around the screen, but it doesn't feel "real." And now, with the advent of generative AI, it's all too easy to create digital "art" that approximates, and, eventually, surpasses what the best digital artists can create.
Crossing the chasm—where digital representations have the potential to map onto something in the physical world—that feels uniquely satisfying.
The Setup

Emergent complexity
Today, I’m going to show you how to define a series of concentric circles and then apply Perlin noise to create some fun, organic designs reminiscent of tree rings. Your job will be to take this foundation and make it your own.
Contrary to what most people believe, developers rarely use math in their day-to-day work. There are exceptions, but most of the complicated math is abstracted away.
So, no, you don't need to be comfortable with mathematics or trigonometry for this tutorial. In fact, you don't necessarily need any coding skills. Languages like Processing were designed to be beginner-friendly, and javascript has a great library called p5.js designed specifically for algorithmic art.
Since I'm comfortable with Python, vsketch is my preferred tool and the one I’ll be using in this tutorial.
Regardless of which language or framework you choose, the logic is the same. You can take any of these examples and have AI translate them for you.
The Iterative Framework
Before we jump into the code, it's important to point out that, while experimentation is important, you should have a rough idea of how to iterate your way to the design you have in mind.
Think of it this way: you're defining a structure, then you're altering it. If you can't see the general structure underlying a design, you have a problem that no amount of randomness will resolve.
Let's say you want to design a wavy line that crosses the screen in an organic way. At first, this looks like a complex structure. What you need to understand is that that wavy line starts with a basic definition. Whether that basic structure is a vector from (x1,y1) → (x2,y2), or a more complex function, it doesn't really matter. It was defined and static.
Great, so how do you make that straight line "come alive"? The next step is to define the line as a series of points, let's say 200, and connect them. Visually, you have the same straight line. Functionally, however, you have 200 separate lines. What if you were to move each point independently?
This is where noise is applied to your neatly defined structure. If you move each of those points some discrete distance using Perlin noise, you now have an organic-looking wavy line (although you might have to play with the variables to hone in on what you're intending to create).
Disturbed Concentric Circles
Now that we have a framework for how to think about this, let's see it in action.
I'll share the complete code example at the end. The following snippets are for demonstration purposes and won't work on their own.
Step 1: Define a circle

Boring!
The first thing we need is a circle. Err, visually it’s a circle. In fact, it’s a 360-sided polygon with points laid out in a circular path:
radius = 0.2
circle_points = []
for point in range(360):
# calculate the angle for each point on a circular path
angle = point * math.pi / 180
# calculate the coordinate for each point
x = radius * math.cos(angle)
y = radius * math.sin(angle)
# add coordinate to a list
circle_points.append((x, y))
# draw the shape
vsk.polygon(circle_points, close=True)Step 2: Add concentric circles

Getting somewhere…
Now that we have a single “circle,” let’s add more layers.
radius = 0.2
layers = 20 # new
for layer in range(1, layers): # new loop, start at 1
circle_points = []
for point in range(360):
angle = point * math.pi / 180
# multiple by layers for expanding radius
x = radius * layer * math.cos(angle)
y = radius * layer * math.sin(angle)
circle_points.append((x, y))
# draw the shape
vsk.polygon(circle_points, close=True)Step 3: Noise, Part I

Cool, but not exactly what we’re going for.
Those circles are looking far too sharp. Let’s add some noise to the system.
First, let’s do it the “wrong” way for demonstration purposes.
noise = vsk.noise(layer * self.noise_scale,
point * self.noise_scale)
x = self.radius * layer * math.cos(angle) * noise
y = self.radius * layer * math.sin(angle) * noiseYou’ll notice that this does indeed skew the circles in an organic way. But, there’s an unintended side effect: the “circles” don’t complete the loop.
So how do we fix this?
Step 4: Noise, Part II

Concentric circles with radial noise sampling
In order to “complete the loop” and apply asymmetry, we need to adjust our noise value.
Rather than simply sampling by layer and degree, we need to account for the layer and x and y coordinates independently. This allows us to sample the noise function along a circular path in noise-space.
It looks like this:
noise_offset = 7 # arbitrary number
# Sample noise on a circle so it loops naturally
n_layer = layer * self.noise_scale
n_x = (noise_offset + math.cos(angle)) * self.noise_scale * 100
n_y = (noise_offset + math.sin(angle)) * self.noise_scale * 100
noise = vsk.noise(n_layer, n_x, n_y)Explanation
noise_offset: Shifts the sampling circle away from the origin of noise space. Without it, you end up with a symmetrical shape due to the underlying lattice structure of the noise algorithm.
noise_scale: Controls the frequency of the noise — how quickly values change as you move through noise space. Lower values produce smoother, more gradual variation; higher values produce more rapid, detailed variation. I typically default to 0.001 and adjust from there.
n_layer: The first dimension of the 3D noise lookup. Here, it separates each concentric layer so they sample from different "slices" of noise space, giving each ring a unique shape.
n_x: The second dimension of the noise lookup, driven by
cos(angle). Combined with n_y, this traces a circle in noise space.n_y: The third dimension of the noise lookup, driven by
sin(angle). The circular path ensures the noise loops seamlessly.“* 100”: Increases the radius of the sampling circle in noise space, resulting in more variation (more "bumps") around each ring. You could parameterize this, but I find it easiest to hard code.
Step 5: That’s up to you!

Plotter Art: Concentric circles with asymmetric noise and rotation
Now that you have your noisy concentric “circles,” which beautifully resemble the rings of a tree, you can experiment with all sorts of variations. You’ve defined your structure. Now you get to disturb it.
Some ideas to explore:
What if you applied a slight rotation to each layer?
What if each layer had a slightly different number of sample points? (e.g., fewer points on inner rings, more on outer rings — or vice versa)
What if you layered two different noise frequencies together — one for large, sweeping distortions and another for fine, high-frequency texture?
What if the noise amplitude increased (or decreased) as the layers moved outward, so inner rings are smooth and outer rings are wild?
What if you added a second shape at the center that uses the inverse of the noise values, creating a kind of positive/negative space interplay?
Full code example (Python/vsketch)
import vsketch
import math
class RadialNoiseTutorialSketch(vsketch.SketchClass):
# Sketch parameters:
radius = vsketch.Param(0.200)
points = vsketch.Param(360)
layers = vsketch.Param(10)
noise_scale = vsketch.Param(0.001)
noise_offset = vsketch.Param(10) # Offset to break symmetry
def draw(self, vsk: vsketch.Vsketch) -> None:
vsk.size("letter", landscape=True)
vsk.scale("cm")
for layer in range(1, self.layers + 1):
circle_points = []
for point in range(self.points):
angle = point * math.pi / 180
# Sample noise on a circle so it loops naturally
n_layer = layer * self.noise_scale
n_x = (self.noise_offset + math.cos(angle)) * \
self.noise_scale * 100
n_y = (self.noise_offset + math.sin(angle)) * \
self.noise_scale * 100
noise = vsk.noise(n_layer, n_x, n_y)
x = (self.radius * layer) * \
math.cos(angle) * noise
y = (self.radius * layer) * \
math.sin(angle) * noise
circle_points.append((x, y))
vsk.polygon(circle_points, close=True)
def finalize(self, vsk: vsketch.Vsketch) -> None:
vsk.vpype("linemerge linesimplify reloop linesort")
if __name__ == "__main__":
RadialNoiseTutorialSketch.display()
