Nach meinen ersten Schritten mit Generative Art mit Python Pillow, habe ich mir einmal angeschaut, wie ich von Pixelgrafiken weg und hin zu Vektorgrafiken kommen kann und bin dabei auf PyCairo gestoßen.
Hier kann man in wenigen Schritten Vektorgrafiken erzeugen.

Gar nicht so übel für den Anfang. Wie ist der Weg dahin? Wir brauchen wieder wenige Module, um das ganze zu erschaffen. pycairo
und random
.
import cairo
import random
Und schon kann’s losgehen. Wir erstellen eine .svg
Datei mit dem gewünschten Dateinamen und den gewünschten Maßen. Hier erstelle ich nested_squares.svg
mit den Maßen 512x512
. Cairo arbeitet mit sogenannten surfaces und contexts. Die Datei, die wir erstellen, ist ein surface, also eine Oberfläche. Auf dieser Oberfläche möchten wir malen. Dies tun wir mit context.
with cairo.SVGSurface("nested_squares.svg", 512, 512) as surface:
context = cairo.Context(surface)
Und was brauchen wir zum Malen? Wir geben uns ersteinmal eine Hintergrundfarbe. Farben in pycairo geben wir im sogenannten XYZColor Format an. Heißt, wir nutzen RGB, aber mit einer Skala von 0 bis 1 für die einzelnen Farben. Das bedeutet, Weiß ist (1, 1, 1)
und Schwarz (0, 0, 0)
. Wie schon in Python Pillow, wird der Hintergrund ganz einfach als rectangle
über die gesamte Context-Größe erstellt. Zuletzt füllen wir das gemalte Rechteck mit der gesetzten Farbe. (Das alles findet im with
Kontext statt; also immer schön an die Einrückung denken!) Dies sieht dann so aus:
context.set_source_rgb(0, 0, 0)
context.rectangle(0, 0, 512, 512)
context.fill()

WOW! Was ein Hingucker! Schnell was drüberwerfen und nochmal schauen. Farbe verändert, gleiche Rechteck-Größe, aber diesmal nur die Umrandung mit stroke()
.
context.set_source_rgb(1, 0.1, 0.4)
context.rectangle(0, 0, 512, 512)
context.stroke()

Viel besser. Wie teilen wir das jetzt in ein Grid auf? Lass uns ein Schachbrett versuchen. Dazu iterieren wir über 8 Zeilen und 8 Spalten und möchten jeweils wieder ein Rechteck malen. 8er-Grid ist einfach. Wir nehmen die Größe des Context (512) und teilen durch 8. Dazu fahren wir auf unserem Context mit .move_to()
an einen bestimmten Punkt. In Iteration 1 ist dies (0, 0)
, da unsere range(8)
Listen bei 0
beginnen. In den weiteren Iterationen bewegen die sich dann entsprechend immer ein achtel der Context-Größe nach rechts, bis das Ende einer Zeile erreicht ist. Dann geht’s über die y
-Schleife eine Zeile nach unten.
Was machen wir nun, wenn wir an dem Punkt angekommen sind? Wir erstellen wieder ein Rechteck mit .rectangle
. Dieses Mal beginnen wir aber nicht fix bei (0,0)
sondern bei genau dem Punkt, zu dem wir mit .move_to
gegangen sind. Mit context.get_current_point()
kriegen wir diese Koordinaten raus. Der *
davor ist eine sehr praktische Python-Funktion, mit der Tupel adhoc entpackt werden können. Statt also context.get_current_point()[0]
für die x-Koordinate rauszufinden, können wir mit dem * direkt x und y des current_point
in die .rectangle
Funktion einbetten. Wie groß soll das Rechteck sein? Natürlich unserem Grid angepasst ein achtel der Gesamtgröße. Dann, wie vorhin auch schon, wird mit .stroke()
gezeichnet.
for x in range(8):
for y in range(8):
context.move_to(x * 512/8, y * 512/8)
context.rectangle(*context.get_current_point(), 512/8, 512/8)
context.stroke()

Fantastisch. Jetzt nur noch die schönen Muster, oder? Hier fing dann mein Kopfzerbrechen an. Meine Inspiration, dieses Muster nachzubauen, kam aus einem YouTube-Video von Coding Cassowary. Coding Cassowary benutzt aber nicht PyCairo sondern turtle
. Der feine aber entscheidende Unterschied? PyCairo zeichnet Rechtecke vom Ankerpunkt oben links (Koordinate (0,0)
). Turtle hingegen hat den Ankerpunkt eines Rechtecks zentriert in der Mitte. Letzteres ist für diese Aufgabe ziemlich von Vorteil. Man wackelt in die Mitte eines Schachfelds, bewegt sich ein bisschen in irgendeine Richtung und zeichnet ein neues Rechteck, was etwas kleiner ist, als das Schachfeld. Mit PyCairo rechnen wir aber immer vom oberen linken Punkt, weshalb ab der eigentlich spannenden Stelle das Tutorial für mich leider nutzlos wurde. Was tun wir also?
Zuerst müssen wir in der obigen Schleife noch eine Zeile hinzufügen, die nach jedem .move_to
die Koordination speichert. Da müssen wir nämlich gleich drauf zugreifen. Der Rest des Code-Schnipsels bleibt gleich.
...
context.move_to(x * 512/8, y * 512/8)
current_point = context.get_current_point()
...
Jetzt wird’s spannend! Zuerst möchten wir den Zufalls-Teil sicherstellen. Mit sogenanntem Noise bringen wir ein wenig Variabilität in’s Spiel. Hierzu nutzen wir die random.uniform()
Funktion. Diese wirft zwischen den genannten Grenzen eine Zufallszahl aus. Hierbei handelt es sich um eine Gleichverteilung.
Im Anschluss starten wir eine weitere Schleife. Diesmal über eine range(1, 5 + 1)
. 5 ist hierbei die Zahl der “Pyramiden” (oder “Nested Squares”, die wir haben wollen. Wir setzen unsere aktuellen Koordinaten auf die im vorigen Code-Schnipsel gespeicherten und bewegen uns von da aus relativ (.rel_move_to
) um eine zufällige Zahl random_noise
nach rechts unten. Die Zahl wird mit jeder Iteration weiter mit der Iterations-Variable i
multipliziert. So geht unser Zeichenstift also jede Iteration weiter vom Ursprungspunkt weg.
An dieser Stelle legen wir nun noch die Maße des nächsten Rechtecks fest. Dieses soll natürlich kleiner sein als das jeweils vorige, weshalb auch hier in der Formel die Iterations-Variable auftaucht (512/8) - (i * 10)
. Wie groß ist das Rechteck also? Ein Achtel der Gesamtbreite ist unser Schachfeld. (i * 10)
kleiner ist das Rechteck. (Die 10 ist übrigens “die gleiche”, wie die 10 bei unserem random_noise
Generator – aber dazu gleich mehr).
Jetzt wird das Rechteck erstellt und dann gezeichnet. Et voila!
random_noise = random.uniform(1, 10)
for i in range(1, 5 + 1):
context.move_to(*current_point)
context.rel_move_to(i * random_noise, i * random_noise)
rect_w, rect_h = (512/8) - (i * 10), (512/8) - (i * 10)
context.rectangle(*context.get_current_point(), rect_w, rect_h)
context.stroke()

Na schau mal einer an! Schon sind wir fertig. In meinem Experiment lief das bei Weitem nicht so einfach. Da musste ich viel hin und her probieren, wie das Verhältnis zwischen Bildgröße, Gridgröße und den Variablen, wie Noise und Pyramiden-Zahl sein muss. Dabei bin ich auf eine sehr schöne Auflösung gestoßen. Das wichtigste an dem ganzen Spaß scheint die noise
Variable zu sein. Diese hat nun ein Verhältnis zu allen anderen Variablen. Ich erstze also alle hart-kodierten Zahlen durch die Variablen und erhalte folgenden Gesamt-Code
import cairo
import random
size = 512
grid = 8
pyramids = 5
noise = size/grid/pyramids
with cairo.SVGSurface("nested_squares.svg", size, size) as surface:
context = cairo.Context(surface)
context.set_source_rgb(0, 0, 0)
context.rectangle(0, 0, size, size)
context.fill()
context.set_source_rgb(1, 0.1, 0.4)
context.rectangle(0, 0, size, size)
context.stroke()
for x in range(grid):
for y in range(grid):
context.move_to(x * size/grid, y * size/grid)
current_point = context.get_current_point()
rect_w, rect_h = size/grid, size/grid
context.rectangle(*context.get_current_point(), rect_w, rect_h)
context.stroke()
random_noise = random.uniform(1, noise)
for i in range(1, pyramids + 1):
context.move_to(*current_point)
context.rel_move_to(i * random_noise, i * random_noise)
rect_w, rect_h = (size/grid) - (i * noise), (size/grid) - (i * noise)
context.rectangle(*context.get_current_point(), rect_w, rect_h)
context.stroke()
surface.write_to_png("nested_squares.png")
Die letzte Zeile speichert die Datei zur einfachen Verwertung noch zusätzlich als PNG
ab. Spannender ist aber mit Sicherheit die noise
Variable. Damit ich auf unterschiedlichen Canvas-Größen arbeiten kann, habe ich sie als Verhältnis von Gesamtgröße zu Grid zu gewünschten Pyramiden erstellt. Scheint auf den meisten Größen zu funktionieren. Mit size
, grid
& pyramids
kann man dann nach belieben rumspielen. Hier einmal der Unterschied zwischen grid = 12, pyramids = 6
, grid = 4, pyramids = 10
und unseren bisherigen Werten grid = 8, pyramids = 5
.



Wie man sieht, funktioniert die Noise-Verteilung recht gut auch auf unterschiedlichen Variablen. Damit ist mein Ziel erreicht!
Das Titelbild des Posts ist übrigens mit size = 2048, grid = 40, pyramids = 5
entstanden. Einfach nur fantastisch! Zum Abschluss noch ein Ausblick, wo das hinführen kann. Leichter Bruch in der Farbwahl, aber was kann ich dafür, was der Randomisierer an Farben ausspuckt 😉 Hier Pastell Hintergrund mit weniger Pastell Pyramiden. Passt extrem gut mit den Mustern, würde ich sagen!

Das ist nur der Anfang meiner Reise in die unendlichen Tiefen der Generative Art. Komm mit mir und tausch Dich gern mit mir aus.