Over the past few months, I have been filling my Instagram (@new_pathway) with images from various generative art experiments, but I have never actually shown the code behind them. This is the first attempt to change that.
Today I want to walk through what I am calling the yarn algorithm: a system that draws entangled strands across a canvas, built in Python. Along the way I will show some of the project structure, a few code snippets, and a rounding bug that took me longer to find than I would care to admit.
The concept: yarn, threads, and weaving
The idea was to create lines that look like actual yarn, each line made up of smaller threads, each thread split into segments that weave under and over one another. Visually, it suggests layers of fibre crossing a page, fraying at the edges, shifting colour across its length.
The structure has three levels:
- Yarn: the overall piece of string, described by a cubic Bezier curve running across the canvas.
- Threads: each yarn is subdivided into multiple threads, each following a sub-section of the main curve.
- Segments: each thread is sliced into smaller over/under pieces to simulate weaving. Each segment gets a z-index so the drawing routine knows which is on top.
Everything gets stored in a single list, sorted by z-index at draw time. Simple in principle; moderately fiddly in execution.
Project structure
I like to separate logic into distinct files:
- yarn.py: the Yarn and Thread classes, plus helpers for slicing Bezier curves and offsetting control points.
- main.py: the entry point: set up a canvas, create Yarn objects, generate threads and segments, draw.
- utils.py: colour generation and geometric helpers that would otherwise clutter the main classes.
The Yarn class looks roughly like this:
class Yarn:
def __init__(self, yarn_path, **kwargs):
self.yarn_path = yarn_path # a single CubicBezier
self.number_of_threads = kwargs.get("number_of_threads", 10)
self.number_of_twists = kwargs.get("number_of_twists", 20)
self.thickness_of_thread = kwargs.get("thickness_of_thread", 1.5)
self.base_colour = kwargs.get("base_colour", "#0000FF")
self.colour_variation = kwargs.get("colour_variation", 0.2)
self.threads = []
def generate_threads(self):
# picks random start/end sub-paths and creates Thread objects
return self.threads
The full file is in my GitHub repo @galiquis if you want the detail.
Colour: the HSL trick
Because each yarn contains multiple threads, I wanted gentle colour variety across a piece, different shades of the same teal, or small variations within a burnt umber palette. The approach: define three base colours, then shift the HSL (Hue, Saturation, Lightness) values slightly for each new yarn or thread.
The base palette I used:
- Dark Teal:
#004B49 - Sea Blue:
#0082C9 - Burnt Umber:
#8A3324
Small shifts from those anchors keep the piece coherent while giving it enough variation to feel alive. The function signature looks like:
def vary_color_hsl(hex_color, max_hue_shift=0.05, max_sat_shift=0.1):
# parse rgb from hex, convert to HLS, shift, clamp, return new hex
return new_hex_color
The rounding gotcha
Midway through generating several hundred yarns, everything jammed into the top ten percent of the canvas, nowhere near the sprawling fibre tapestry I had imagined. I checked random offsets, control points, curve generation. Everything looked fine.
The culprit was this line:
spacing = canvas_size[1] // (number_of_yarns - 1)
That double-slash is Python’s integer division operator. For large numbers of yarns, it was rounding the spacing down to 1 or even 0, meaning each successive yarn was placed practically on top of the last. The fix:
spacing = canvas_size[1] / (number_of_yarns - 1)
One character. The yarn lines spread gracefully across the full height of the canvas. It is a perfect example of how a seemingly trivial arithmetic choice can produce large-scale artistic chaos, and why testing with real parameters matters.
Drawing the final piece
When all the geometry is ready, I collect every segment from every yarn into a master list, sort by z-index, and feed them into the drawing routine (using aggdraw, or sometimes svgwrite):
all_segments.sort(key=lambda seg: seg["z_index"])
draw_shapes_dict(canvas, all_segments, (0, 0))
Each segment carries its curve geometry, stroke colour, stroke width, and fill opacity (usually zero, it is just a line). The weaving effect comes from the z-index sorting: some threads are drawn on top of others, giving the illusion of actual over-under crossing.
What I took away
Three things, in order of decreasing obviousness:
- Be careful with integer division. It almost never does what you want in floating-point geometry.
- Subtle geometry, small curve offsets, gentle frays at thread ends, is often what separates work that feels alive from work that feels generated.
- Working in small, consistent classes makes experimentation much easier. Yarn, Thread, Segment: each has one job. Changing how colour varies does not require touching the drawing code.
More posts in this series to come, covering colour algorithms, fractal curves, and the occasional glitch that turned into something better than what I was trying to make. The Instagram (@new_pathway) has the visuals; this is where the code lives.
Related: Why My Code Needs a Compass