A polyglot collection of composable generative art tools, with a focus on 2D computational geometry.
This project contains a mix of Rust, C++, and Python. It's primarily Rust.
Install dependencies with
# Python dependencies
python3 -m venv --prompt generative .venv
source .venv/bin/activate
python3 -m pip install -r requirements.txt
# C++ dependencies
sudo apt install build-essential cmake ninja-build
git submodule update --init --recursive
The Rust build has been configured to also perform the C++ CMake build, so all you need is
cargo build
If you don't want to build the C++ parts, you can do
cargo build --no-default-features
but note that this will disable building the geom2graph tool.
You can run the Python tests with
source .venv/bin/activate
pytest
and the Rust tests with
cargo test
If the C++ tests have been enabled with --all-features, or --features=cxx-tests, they are copied
to target/debug/cxx-tests.
Throughout this entire document, it is assumed that each of the tool binaries has been added to your PATH with
export PATH=$PWD/target/debug/:$PATH
There is a generative Rust/C++/Python library, but the user is expected to use
the CLI tools instead. I'm enamored with the Unix philosophy, so each tool does its best
to produce/consume a standard textual interface.
* Each tool read/writes to/from stdin/stdout
* Logging is done to stderr
* Geometries are in WKT
format, one geometry per line
* Graphs are in TGF format
See the attractor tool for more details on how to generate these attractor images.







The following snippet generates random asemic writing glyphs
glyphs() {
local glyph_kind="$1"
local number="$2"
local size="$3"
{
for _ in $(seq "$number"); do
$glyph_kind "$size"
done
} | pack --width 1000 --height 1000 --padding 20
}
To generate random glyphs, we'll: 1. Take a geometry graph 2. Perform random traverals of the graph 3. (optionally) Smooth each traversal into a curve
random_rounded() {
local size="$1"
point-cloud --log-level WARN --domain unit-square --points 15 --scale 6 |
urquhart --output-format tgf |
traverse --log-level WARN --traversals 5 --length 5 --untraversed |
transform --scale="$size" |
smooth --iterations 4 |
bundle
}
glyphs random_rounded 90 10 | wkt2svg --output $ASEMIC_RANDOM_ROUNDED
The
--untraversed points could be replaced with diacritical marks.
We could also use the triangulation of a random point cloud
random_triangulated() {
local size="$1"
point-cloud --log-level WARN --domain unit-square --points 10 --scale 6 |
triangulate --output-format tgf |
traverse --log-level WARN --traversals 3 --length 3 --remove-after-traverse |
transform --scale="$size" |
smooth --iterations 4 |
bundle
}
glyphs random_triangulated 100 10 | wkt2svg --output $ASEMIC_RANDOM_TRIANGULATED
Neither of these approaches give a coherent sense of self-similarity that's necessary for linguistic glyphs. If, instead of using a random point cloud, we use a regular grid, that dramatically changes the sense of self-similarity.
grid_rounded() {
local size="$1"
grid --output-format graph --width=2 --height=3 |
traverse --log-level WARN --traversals 5 --length 5 --remove-after-traverse |
transform --scale="$size" |
smooth --iterations 4 |
bundle
}
glyphs grid_rounded 120 20 | wkt2svg --output $ASEMIC_GRID_ROUNDED
We can also reduce the number of smoothing iterations to get beveled corners
grid_beveled() {
local size="$1"
grid --output-format graph --width=2 --height=3 |
traverse --log-level WARN --traversals 5 --length 5 --remove-after-traverse |
transform --scale="$size" |
smooth --iterations 1 |
bundle
}
glyphs grid_beveled 120 20 | wkt2svg --output $ASEMIC_GRID_BEVELED
We could also regular triangle grid, to get loopier results, with no vertical lines
grid_triangulated() {
local size="$1"
grid --grid-type triangle --output-format graph --width=2 --height=3 |
traverse --log-level WARN --traversals 4 --length 5 --remove-after-traverse |
transform --scale="$size" |
smooth --iterations 4 |
bundle
}
glyphs grid_triangulated 100 20 | wkt2svg --output $ASEMIC_GRID_TRIANGULATED
Using a slanted grid makes almost compelling cursive, if only the jarring horizontal lines were removed.
grid_jagged() {
local size="$1"
grid --grid-type ragged --output-format graph --width=2 --height=3 |
traverse --log-level WARN --traversals 4 --length 5 --remove-after-traverse |
transform --scale="$size" |
smooth --iterations 4 |
bundle
}
glyphs grid_jagged 100 20 | wkt2svg --output $ASEMIC_GRID_JAGGED
If we use a radial grid with no point filling between the spokes, smooth after the traversals, we results without much self-symmetry.
grid_radial() {
local size="$1"
grid --grid-type radial --output-format graph --width=5 --height=3 |
traverse --log-level WARN --traversals 5 --length 5 --remove-after-traverse |
transform --scale="$size" |
smooth --iterations 4 |
bundle
}
glyphs grid_radial 75 10 | wkt2svg --output $ASEMIC_GRID_RADIAL
If instead we use a radial grid with a high point density along the rings, we get more compelling glyphs reminiscent of a circular maze.
grid_radial_dense() {
local size="$1"
grid --grid-type radial --output-format graph --width=5 --height=4 --ring-fill-ratio=0.7 |
traverse --log-level WARN --traversals 10 --length 30 |
transform --scale="$size" |
bundle
}
glyphs grid_radial_dense 64 10 | wkt2svg --output $ASEMIC_GRID_RADIAL_DENSE
examples/random-lsystems/saved.json contains parameters for randomly (pre)generated Lindenmayer systems.
for i in $(seq 0 13); do
jq ".[$i]" examples/random-lsystems/saved.json |
tools/parse-production-rules.py -c - -n "$(jq ".[$i].iterations" examples/random-lsystems/saved.json)" |
tools/interpret-lstring.py -l ERROR -a "$(jq ".[$i].angle" examples/random-lsystems/saved.json)" |
tools/project.py --scale "$(jq ".[$i].scale" examples/random-lsystems/saved.json)" --kind pca |
wkt2svg --output "examples/random-lsystems/random-$i.svg"
done
The parse-production-rules.py tool takes a set of production rules, and a starting axiom, and
interprets the rules on the axiom for some specified number of iterations
$ ./tools/parse-production-rules.py --rule 'a -> ab' --rule 'b -> a' --axiom a --iterations 3
abaab
This tool supports context-free, stochastic, and context-sensitive grammars, with rules of the form
[left_context<] lhs [>right_context] [:probability] -> rhs
#ignore: tok1,tok2,tok3
The [] square brackets denote optional parts of the production rule.
The random-production-rules.py tool generates a random set of production rules in JSON form that
parse-production-rules.py --config knows how to read.
$ ./tools/random-production-rules.py --seed 4290989563 |
./tools/parse-production-rules.py --config - --iterations 3
|v]->^][<>^[[
These L-strings can then be interpreted with a 3D turtle. Each symbol controls the turtle's motion through space.
$ ./tools/parse-production-rules.py --config ./examples/lsystems/sierpinski-tree.json |
./tools/interpret-lstring.py |
tail -n 4
LINESTRING Z (0 -126.48885271170681 224.54772721475285, 0 -125.48885271170681 224.54772721475285)
LINESTRING Z (0 -128.61017305526647 226.6690475583125, 0 -125.61017305526647 226.6690475583125, 0 -124.90306627407992 225.96194077712596)
LINESTRING Z (0 -125.61017305526647 226.6690475583125, 0 -124.61017305526647 226.6690475583125)
LINESTRING Z (0 -125.61017305526647 226.6690475583125, 0 -124.90306627407992 227.37615433949907)
Notice that the geometries are in 3D WKT. They can be rendered in an interactive 3D OpenGL viewer (render.py) or projected to 2D (project.py before being converted to SVG with wkt2svg.
./tools/parse-production-rules.py --config ./examples/lsystems/sierpinski-tree.json |
./tools/interpret-lstring.py |
./tools/project.py --kind=yz |
wkt2svg --output ./examples/lsystems/sierpinski-tree.svg
random-production-rules.py generates a great many duds (See
#83), so random-lsystem.sh is an easy way of
generating and visualizing random L-Systems quickly.
$ ./tools/random-lsystem.sh
2024-03-03 08:57:05,580 - tools/random-production-rules.py - INFO - Using random seed 1063093925
{"seed": 1063093925, "rules": ["G -> [>[v[[|v-|<F>GG[v", "G -> v]<|", "F -> <^v"], "axiom": "G"}
2024-03-03 08:57:05,926 - tools/render.py - INFO - Loaded 64 segments and 0 points.

The generation-type tools generate input data for other tools to consume.
point-cloud is a tool that generates random points in the unit circle or square.
$ point-cloud --points 4 --domain unit-circle --scale 100 --seed 15838575381579332872
POINT (30.224877936836876 -70.83712102787706)
POINT (-38.04972657419976 -33.95658816921603)
POINT (-1.7494655022386558 -3.0116273192492646)
POINT (-4.305088398836983 10.443819974018535)
The grid tool generates different kinds of grids:
* triangle
* quad
* ragged
* hexagon
* radial
and supports outputting the resulting grid in TGF graph format, WKT POINTs, or WKT LINESTRINGs.
```sh $ grid --output-format graph --grid-type quad --width 1 --height 1 0 POINT(0 0) 1 POINT(1 0) 2 POINT(0 1) 3 POINT(1 1)
0 2 0 1 1 3
$ claude mcp add generative \
-- python -m otcore.mcp_server <graph>