MCPcopy Index your code
hub / github.com/Notgnoshi/generative

github.com/Notgnoshi/generative @main

Chat with this repo
repository ↗ · DeepWiki ↗ · + Follow
610 symbols 2,099 edges 101 files 84 documented · 14%
What it actually does AI analysis from the code graph — generated when you open this
loading…
README

Generative Art

GitHub Actions status GitHub Actions status GitHub Actions status Code style: black

A polyglot collection of composable generative art tools, with a focus on 2D computational geometry.

Table of contents

How to build

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

Philosophy

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

Examples

Chaotic attractors

See the attractor tool for more details on how to generate these attractor images.

Asemic writing

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

Random L-Systems

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

examples/random-lsystems/random-0.svg

examples/random-lsystems/random-10.svg

examples/random-lsystems/random-11.svg

examples/random-lsystems/random-12.svg

examples/random-lsystems/random-13.svg

examples/random-lsystems/random-1.svg

examples/random-lsystems/random-2.svg

examples/random-lsystems/random-3.svg

examples/random-lsystems/random-4.svg

examples/random-lsystems/random-5.svg

examples/random-lsystems/random-6.svg

examples/random-lsystems/random-7.svg

examples/random-lsystems/random-8.svg

examples/random-lsystems/random-9.svg

The tools

Lindenmayer Systems

parse-production-rules.py

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.

random-production-rules.py

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]->^][<>^[[

interpret-lstring.py

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-lsystem.sh

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.

random-lsystem.gif

Generation

The generation-type tools generate input data for other tools to consume.

point-cloud

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)

grid

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

Extension points exported contracts — how you extend this code

MapCoordsInPlaceMut (Interface)
(no doc) [11 implementers]
generative/geometry_mut_map.rs
CommandExt (Interface)
(no doc) [1 implementers]
tests/mod.rs

Core symbols most depended-on inside this repo

from_wkt
called by 76
generative/io/wkt.cpp
captured_output
called by 34
tests/mod.rs
tool
called by 34
tests/mod.rs
_parse
called by 26
generative/lsystem/production.py
cend
called by 26
generative/include/generative/geometry-flattener.h
rewrite
called by 25
generative/lsystem/grammar.py
flatten
called by 25
generative/flatten/flatten.py
end
called by 25
generative/include/generative/geometry-flattener.h

Shape

Function 293
Method 223
Class 74
Enum 18
Interface 2

Languages

Rust50%
Python34%
C++16%

Modules by API surface

tests/test_flatten.py26 symbols
tests/test_grammar.py25 symbols
generative/snap.rs25 symbols
generative/io/wkt.rs25 symbols
tests/test_production.py21 symbols
tools/streamline.rs19 symbols
tools/grid.rs18 symbols
tests/test_wkio.py18 symbols
tools/wkt2svg.rs17 symbols
generative/dla.rs15 symbols
tests/test_interpreter.py14 symbols
generative/triangulation.rs14 symbols

For agents

$ claude mcp add generative \
  -- python -m otcore.mcp_server <graph>

⬇ download graph artifact