Hobby Hilbert Simplex

Friday 26 September 2025

An exploration and explanation of how to generate interesting swoopy art

I saw a generative art piece I liked and wanted to learn how it was made. Starting with the artist’s Kotlin code, I dug into three new algorithms, hacked together some Python code, experimented with alternatives, and learned a lot. Now I can explain it to you.

It all started with this post by aBe on Mastodon:

I love how these lines separate and reunite. And the fact that I can express this idea in 3 or 4 lines of code.

For me they’re lives represented by closed paths that end where they started, spending part of the journey together, separating while we go in different directions and maybe reconnecting again in the future.

#CreativeCoding #algorithmicart #proceduralArt #OPENRNDR #Kotlin

80 wobbly black hobby curves with low opacity. In some places the curves travel together, but sometimes they split in 2 or 3 groups and later reunite. Due to the low opacity, depending on how many curves overlap the result is brighter or darker.

The drawing is made by choosing 10 random points, drawing a curve through those points, then slightly scooching the points and drawing another curve. There are 40 curves, each slightly different than the last. Occasionally the next curve makes a jump, which is why they separate and reunite.

Eventually I made something similar:

Along the way I had to learn about three techniques I got from the Kotlin code: Hobby curves, Hilbert sorting, and simplex noise.

Each of these algorithms tries to do something “natural” automatically, so that we can generate art that looks nice without any manual steps.

Hobby curves

To draw swoopy curves through our random points, we use an algorithm developed by John Hobby as part of Donald Knuth’s Metafont type design system. Jake Low has a great interactive page for playing with Hobby curves, you should try it.

Here are three examples of Hobby curves through ten random points:

The curves are nice, but kind of a scribble, because we’re joining points together in the order we generated them (shown by the green lines). If you asked a person to connect random points, they wouldn’t jump back and forth across the canvas like this. They would find a nearby point to use next, producing a more natural tour of the set.

We’re generating everything automatically, so we can’t manually intervene to choose a natural order for the points. Instead we use Hilbert sorting.

Hilbert sorting

The Hilbert space-filling fractal visits every square in a 2D grid. Hilbert sorting uses a Hilbert fractal traversing the canvas, and sorts the points by when their square is visited by the fractal. This gives a tour of the points that corresponds more closely to what people expect. Points that are close together in space are likely (but not guaranteed) to be close in the ordering.

If we sort the points using Hilbert sorting, we get much nicer curves. Here are the same points as last time:

Here are pairs of the same points, unsorted and sorted side-by-side:

If you compare closely, the points in each pair are the same, but the sorted points are connected in a better order, producing nicer curves.

Simplex noise

Choosing random points would be easy to do with a random number generator, but we want the points to move in interesting graceful ways. To do that, we use simplex noise. This is a 2D function (let’s call the inputs u and v) that produces a value from -1 to 1. The important thing is the function is continuous: if you sample it at two (u,v) coordinates that are close together, the results will be close together. But it’s also random: the continuous curves you get are wavy in unpredictable ways. Think of the simplex noise function as a smooth hilly landscape.

To get an (x,y) point for our drawing, we choose a (u,v) coordinate to produce an x value and a completely different (u,v) coordinate for the y. To get the next (x,y) point, we keep the u values the same and change the v values by just a tiny bit. That makes the (x,y) points move smoothly but interestingly.

Here are the trails of four points taking 50 steps using this scheme:

If we use seven points taking five steps, and draw curves through the seven points at each step, we get examples like this:

I’ve left the points visible, and given them large steps so the lines are very widely spaced to show the motion. Taking out the points and drawing more lines with smaller steps gives us this:

With 40 lines drawn wider with some transparency, we start to see the smoky fluidity:

Jumps

In his Mastodon post, aBe commented on the separating of the lines as one of the things he liked about this. But why do they do that? If we are moving the points in small increments, why do the curves sometimes make large jumps?

The first reason is because of Hobby curves. They do a great job drawing a curve through a set of points as a person might. But a downside of the algorithm is sometimes changing a point a small amount makes the entire curve take a different route. If you play around with the interactive examples on Jake Low’s page you will see the curve can unexpectedly take a different shape.

As we inch our points along, sometimes the Hobby curve jumps.

The second reason is due to Hilbert sorting. Each of our lines is sorted independently of how the previous line was sorted. If a point’s small motion moves it into a different grid square, it can change the sorting order, which changes the Hobby curve even more.

If we sort the first line, and then keep that order of points for all the lines, the result has fewer jumps, but the Hobby curves still act unpredictably:

Colophon

This was all done with Python, using other people’s implementations of the hard parts: hobby.py, hilbertcurve, and super-simplex. My code is on GitHub (nedbat/fluidity), but it’s a mess. Think of it as a woodworking studio with half-finished pieces and wood chips strewn everywhere.

A lot of the learning and experimentation was in my Jupyter notebook. Part of the process for work like this is playing around with different values of tweakable parameters and seeds for the random numbers to get the effect you want, either artistic or pedagogical. The notebook shows some of the thumbnail galleries I used to pick the examples to show.

I went on to play with animations, which led to other learnings, but those will have to wait for another blog post.

Comments

Add a comment:

Ignore this:
Leave this empty:
Name is required. Either email or web are required. Email won't be displayed and I won't spam you. Your web site won't be indexed by search engines.
Don't put anything here:
Leave this empty:
Comment text is Markdown.