I've found Anders Hoff's work on Inconvergent hugely inspiring. In particular his works that are 2D planar geometries (and graphs). This is in part (the second reason why is because of what I do for work) why https://github.com/Notgnoshi/generative exclusively uses the Simple Feature Access geometries as geometric primitives.
Two of his posts that have really captured my imagination have been on Asemic Writing (generative handwriting having no semantic content).
Spline Script gives an algorithm that uses spline fits inside rotated ellipses to generate cursive handwriting. The use of splines is problematic, because all of the tooling that I've built for myself over the last few years has exclusively used WKT as a serialization format for geometries, and while extending WKT to support splines is possible, I haven't found the time nor motivation to build support for handling both splines and discrete geometries in my tooling (I'd want each tool to handle both equally well, which is daunting).
However More Asemic Writing gives an algorithm for generating glyphs more like Hangul and Katakana, which again uses splines, but less "loopy" ones! So instead of taking on the work of providing true spline support, I instead operate on discrete geometries, and then smooth them afterward ;)
This has a few downsides, but the upside is that it's easy.
For the rest of the page, I'll be referring to code from https://github.com/Notgnoshi/generative. It's a mix of Python, C++, and Rust.
# Install the Python dependencies
python3 -m venv --prompt generative .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
# Build the Rust and C++ parts
git submodule update --init --recursive
cargo build --release
# Invoking "cargo run --bin ... -- ..." gets old
export PATH="$PWD/target/release/:$PATH"
Each tool that utilizes random number generation can optionally be seeded, and logs the seed used at
INFO level to stderr
so that the results can be reproduced, even when a seed wasn't given.
For brevity though, I'll leave off the seeds in the shell snippets on this page.
First, generate a uniform random point cloud in a square domain.
$ point-cloud --domain unit-square --points 4 --scale 100
INFO - Generating 4 points with seed 6547691638051612608
POINT (48.0592033866307 60.000131021120076)
POINT (42.74869165920112 57.607049587621795)
POINT (44.23321026420486 2.7466311018484735)
POINT (60.15061084098781 60.0434120953931)
$ point-cloud --domain unit-square --points 12 --scale 300 |
wkt2svg
Second, generate the point cloud's relative neighborhood graph. While not quite equivalent, you can approximate the relative neighborhood by computing the Urquhart graph.
To do this, compute the Delaunay triangulation of the point cloud
$ point-cloud --domain unit-square --points 12 --scale 300 |
triangulate |
wkt2svg
And then remove the longest edge from each triangle
$ point-cloud --domain unit-square --points 12 --scale 300 |
urquhart |
wkt2svg
Third, generate a stroke by randomly traversing the graph. Repeat for some number of times.
$ point-cloud --domain unit-square --points 12 --scale 300 |
urquhart --output-format tgf |
traverse --traversals 3 --length 3 --untraversed |
wkt2svg
Spoiler: you can immediately tell that this will generate quite a bit of nonsense.
Finally, fit splines through some (or all) of the strokes. Here's where I cheat by using Chaikin's smoothing algorithm instead of using splines.
$ point-cloud --domain unit-square --points 12 --scale 300 |
urquhart --output-format tgf |
traverse --traversals 3 --length 3 --untraversed --remove-after-traverse |
smooth --iterations 5 |
wkt2svg
And then repeat a few thousand times and cherry-pick the good results.
I was disappointed in the quality of the results.
So what can we do to make the results better?
We could try using the Delaunay triangulation directly instead of the Urquhart graph.
$ point-cloud --domain unit-square --points 5 --scale 300 |
triangulate --output-format tgf |
traverse --traversals 4 --length 4 --remove-after-traverse --seed 0 |
smooth --iterations 5 |
wkt2svg --padding --output refined-01-delaunay.svg
That could be promising.
We could increase the side of the point cloud and the length of the strokes.
$ point-cloud --domain unit-square --points 30 --scale 300 |
triangulate --output-format tgf |
traverse --traversals 3 --length 10 --random-length --remove-after-traverse |
smooth --iterations 5 |
wkt2svg
This could also be promising! It also used Delaunay triangulation directly. What is it like with the relative neighborhood?
$ point-cloud --domain unit-square --points 30 --scale 300 |
urquhart --output-format tgf |
traverse --traversals 3 --length 10 --random-length --remove-after-traverse |
smooth --iterations 5 |
wkt2svg
It seems as though using the relative neighborhood is more likely to generate strokes that don't intersect, which I don't like.
It seems as though what these glyphs are missing is self-symmetry, or rather, maybe just some kind of "relationship" that the human eye can see. They're too random as-is, so what if we use a regular grid of points instead?
$ grid --output-format graph 3 4 |
traverse --traversals 4 --length 5 --remove-after-traverse |
transform --scale 300 300 |
smooth --iterations 5 |
wkt2svg
Hmm, now this is something where the human eye sees order; I expect that if I were to generate hundreds of these, and lay them next to each other, it'd look coherent.
Something else that we could do is perform fewer iterations of the smoothing algorithm (specifically, only one iteration to add a bevel to each corner).
$ grid --output-format graph 3 4 |
traverse --traversals 4 --length 5 --remove-after-traverse |
transform --scale 300 300 |
smooth --iterations 1 |
wkt2svg
That's cool.
Using a grid added much-needed order, but what if we used triangles instead?
grid --output-format points 3 4 |
triangulate --output-format tgf |
traverse --traversals 4 --length 5 --remove-after-traverse |
transform --scale 100 100 |
smooth --iterations 5 |
wkt2svg
That could probably be compelling. Let's try a few more variations.
grid --output-format points 3 4 |
triangulate --output-format tgf |
traverse --traversals 4 --length 5 --remove-after-traverse |
transform --scale 100 100 |
smooth --iterations 1 |
wkt2svg
grid --output-format points 3 4 |
triangulate --output-format tgf |
traverse --traversals 4 --length 5 --remove-after-traverse |
transform --scale 100 100 |
wkt2svg
I personally find the square grid to be the most compelling variant so far, both with lots of smoothing, and with 45 degree beveled corners.
As is the case with all generative art, there's a million different variations. And as we've seen, small changes in the rules can generate significantly different results. Here's a bunch of things that could be explored next.
transform
tool can do some of this)