Creative Coding mit DrawBot, Teil 1
Wir stellen in den nächsten zwei Tagen programmierte Plakate und animierte Grafiken her. Dabei erlernst du die Grundlagen der Programmierung mit der Sprache Python, einer der gegenwärtig populärsten Programmiersprachen.
In diesem Workshop geht es um programmierte Grafik, das heisst, um die Herstellung von Vektorbildern und Text mit Programmen. Dieser Workshop hat wenig bis gar nichts mit UI-Design zu tun. Die Themen, um die es hier geht, sind Automatisierung und die Rolle digitaler Medien in der Gestaltung. Das klingt sofort nach Untergangsszenario, faktisch geht es aber darum, ein Werkzeug kennenzulernen, mit dem du die Grenzen herkömmlicher Gestaltungs-Software überwinden kannst.
Das Ziel dieses Workshops ist, dass du auf spielerische Weise alle nötigen Grundlagen erlernst, damit du alleine weitermachen kannst, wenn es dir gefällt.
Creative Coding
‘Creative Coding’ ist ein Sammelbegriff für das Programmieren von Software, die keine funktionale, sondern eine expressive Zielsetzung hat, also auf Deutsch: Kunst statt Werkzeug. Das ist eine stark verkürzte Definition – ich bin kein Fan des Begriffs, weil er eine Teilung der Software-Entwicklung in «Zweckgebunden» und «Kreativ» suggeriert.
DrawBot
DrawBot ist der Name einer Software, die ursprünglich von Just van Rossum (Type Designer, doziert an der Königlichen Akademie der Bildenden Künste in den Haag) entwickelt wurde. Die Software wurde/wird vor allem von Frederik Berlaen, einem ehemaligen Studenten Rossums, weiterentwickelt und hat sich unter programmierenden Designern zum Geheimtipp entwickelt.
Python
Python ist der Name einer Programmiersprache, die in den 90er Jahren von Guido van Rossum entwickelt wurde. Seither hat sie sich als eine der beliebtesten Programmiersprachen etabliert. Dank ihrer einfachen Syntax ist sie leichter zu erlernen, als die bis dahin an Hochschulen verbreiteten Sprachen wie Java, Lisp und C – deshalb wird Python in den letzten Jahren an immer mehr Hochschulen (MIT, ETH) verwendet.
Die Einsatzbereiche von Python reichen von Datenanalyse über Web-Entwicklung bis zu Machine Learning.
Python ist auf jedem Apple-Rechner vorinstalliert und kann über das Terminal gestartet werden. Es gibt gegenwärtig zwei Versionen von Python (Nr. 2 und Nr. 3), die sich in teilweise ihrer Syntax unterscheiden. In DrawBot wird Python 3 verwendet.
Syntax
Die «Syntax» bezeichnet bei einer Programmiersprache die Regeln, die du beim Schreiben einhalten musst, damit der Computer dein Programm versteht.
In Python werden Abschnitte im Code mit Umbrüchen und Einrückungen gekennzeichnet, statt mit Klammern und ähnlichem, wie es z.B. in JavaScript der Fall ist. Dadurch ist Python einfacher lesbar als andere Programmiersprachen und damit auch zugänglicher.
Reihenfolge
Generell wird ein Programm von oben nach unten ausgeführt, d.h. der Computer «interpretiert» Zeile für Zeile und tut wie geheissen. Wenn es an einer Stelle ein Problem gibt, so wird das Programm abgebrochen und eine Fehlermeldung ausgegeben.
Dieser Flow von oben nach unten muss nicht komplett linear sein: Abschnitte können ausgelassen werden, wenn bestimmte Bedingungen nicht gegeben sind, sie können durch eine Schleife (engl. ‘Loop’) wiederholt werden oder sie können in Funktionen gespeichert und nur bei Bedarf aufgerufen werden.
Bestandteile eines Programms
Ein typisches Programm in DrawBot kann aus folgenden Bausteinen bestehen:
- Kommentare
- Aufrufe von Funktionen
- Wertzuweisung an Variablen
- Schleifen
- Bedingungen
Es gibt noch weitere Bausteine, wie Funktionsdefinitionen und Klassen, die du für den Einstieg aber nicht brauchst.
Kommentare
Alles was nach einer Raute #
steht, wird vom Computer bis zum Zeilenende ignoriert. Dadurch kannst du Zeilen deaktivieren, ohne sie zu löschen. Andererseits kannst du Kommentare in dein Programm schreiben, die dir helfen, den Überblick zu behalten. Sie helfen dir auch dabei, dein Programm zu verstehen, wenn du es zu einem späteren Zeitpunkt wieder öffnest.
Tipp: du kannst Zeilen mit den Tasaturkombinationen alt shift 9
deaktivieren und alt shift 8
wieder aktivieren.
Mehrzeilige Kommentare können «en bloc» mit drei Anführungszeichen gekennzeichnet werden.
'''
Ein längerer
Absatz voll nützlicher
Hinweise
'''
"""
Auch mit doppelten
Anführungszeichen
geht es.
"""
Funktionen
Funktionen bestehen aus einer Zeichenfolge mit runden Klammern am Ende, z.B. print()
. Es gibt zwei Sorten davon. Die einen liefern einen Wert, z.B. width()
, die anderen haben einen bestimmten Effekt, z.B. oval(0, 0, 100, 100)
.
Manchmal stehen Argumente in den runden Klammern, manchmal nicht.
Variablen
wort = "Plutimikation"
zahl = 42
resultat = 12 + 3
fermente = ["Scoby", "Sauerteig", "Kefir"]
Eine Variable hat einen Namen und einen Wert. Die Wertzuweisung machst du mit einem Gleichzeichen =
. Statt «Wertzuweisung» könnten wir auch sagen, dass wir in einer Variablen einen Wert speichern.
Verwendest du eine Variable ohne Wert, gibt Python eine Fehlermeldung aus.
Traceback (most recent call last):
File "<untitled>", line 1, in <module>
NameError: name 'hallihallo' is not defined
Es gibt verschiedene Arten von Werten, die du in eine Variable speichern kannst.
Strings sind Zeichenketten wie "hello world"
. Strings müssen in Anführungszeichen stehen, sonst meint Python, es seinen Namen von Variablen.
Floats sind Zahlen mit Kommastellen (‘floating point numbers’), wie 3.14
oder 1.0
. Beachte, dass ein Punkt und nicht ein Komma verwendet wird!
Integer sind ganze Zahlen wie 0
oder -32
.
Listen sind mehrere Werte, die durch Kommas getrennt werden, z.B. [1, 22, "hello"]
. Eine Liste wird durch rechteckige Klammern zusammengefasst.
Du kannst auch den Wert, den ein Funktionsaufruf zurückgibt, in eine Variable speichern:
zufallszahl = random()
if zufallszahl > 0.5:
print("Kopf")
else:
print("Zahl")
Du kannst den Wert einer Variablen überschreiben, in dem du ihr einen neuen Wert zuweist.
zahl = 10
print(zahl)
zahl = 12
print(zahl)
Du kannst den Wert einer Variablen stufenweise erhöhen oder vertiefen.
zahl = 0
print(zahl)
zahl = zahl + 1
print(zahl)
zahl = zahl + 1
print(zahl)
zahl = zahl - 2
print(zahl)
Benennung von Variablen
Namen von Variablen müssen mit einem Buchstaben oder Unterstrich _
beginnen. Danach dürfen sie beliebige Buchstaben, Ziffern oder Unterstriche enthalten.
Es gibt verschiedene Arten, Variablen zu benennen, abgesehen von verbotenen Zeichen (Leerzeichen, eine Ziffer als erstes Zeichen etc.) ist es Geschmacksache. Verbreitet sind sowohl myVariable
als auch my_variable
.
Damit du nicht den Überblick verlierst, solltest du möglichst aussagekräftige Namen verwenden wie pos_x
und word_list
im Gegensatz zu foo
und bingoBongo
.
Mit der Funktion print()
in die DrawBot-Konsole schreiben
In DrawBot gibt es unten rechts ein Fenster, das für Text-Output aus deinem Programm reserviert ist. Text kann auf zwei Arten aus deinem Programm kommen: als Fehlermeldung oder weil du die Funktion print()
aufrufst.
print(1 + 1)
print(12 / 3)
print(12 // 3)
print("Hello World")
Die Funktion print()
ist vor allem nützlich, weil du damit an bestimmten Stellen in deinen Programmen die Werte deiner Variablen in der Konsole ausgeben kannst. So kannst du im Auge behalten, ob alles so läuft wie geplant.
Du kannst mehrere Werte als Argumente angeben und mit Kommas trennen. Sie werden bei der Ausgabe aneinandergereiht und mit einem Leerzeichen verbunden.
name = "Bibi"
print("Hallo", name)
print("Das Resultat von 12 : 3 ist", 12//3)
Du kannst zwei Zeichenketten mit einem Plus-Zeichen zusammenziehen.
aroma = "Himbeer"
print("Ich mag", aroma + "bonbons!")
Fehlermeldungen
Zum Einstieg einige Beispiele für Fehler in deutscher Sprache.
- Die Katze ist auf dem Tisch. (richtig)
- Die K@tze ist 4uf dem T1$ch. (falsche Zeichen)
- Katze die ist auf Tisch dem. (falsche Struktur)
Auch wenn es Fehler gibt, ist für uns die Bedeutung einfach zu verstehen. Computer hingegen verstehen gar nichts, sie können nur rechnen.
Auf Python übertragen:
rectWidth = 86
(Korrekt)3rectWidth = 86
(falsche Syntax: erstes Zeichen des Variablennamens ist eine Ziffer)= rectWidth 86
(falsche Syntax, Wertzuweisung kann nicht interpretiert werden.)
In solchen Fällen wird ein Programm nicht weiter ausgeführt, stattdessen wird eine Fehlermeldung ausgegeben. In DrawBot siehst du sie in leuchtendem Rot in der Konsole unten rechts.
So eine Fehlermeldung ist im ersten Moment immer ein Dämpfer. Du wartest ja auf visuellen Output – und dann so etwas. Die Fehlermeldung enthält Informationen darüber, was schief gelaufen ist und wo der Fehler aufgetreten ist. Du solltest sie darum immer lesen, sie hilft dir dabei, dein Programm zu reparieren.
Wann, wo, was
Fehlermeldungen haben in der Regel drei Abschnitte. Auf der Ersten steht, wann die das Problem aufgetreten ist. Auf der Zweiten steht, wo es passiert ist. Auf der dritten Zeile steht, was passiert ist.
Traceback (most recent call last):
File "tryTheImpossible.py", line 1, in <module>
ZeroDivisionError: division by zero
In diesem Beispiel gab es beim letzten Aufruf des Programms ein Problem (‘most recent call last’). Der Fehler ist in einer Datei namens «tryTheImpossible.py» auf der ersten Zeile aufgetreten. Das Problem war der Versuch einer Division durch Null (‘division by zero’).
Dann stehen Dinge drin, die unverständlich sind und die Lesbarkeit erschweren, z.B. Traceback
und <module>
. Der beste Tipp, den ich an dieser Stelle kann, ist, alles auszublenden, was nicht mit Zeilennummer und Art des Fehlers zu tun hat.
Rechnen
In deinen Programmen wird es viel zu rechnen geben: Wie viel Platz ist übrig? Wie breit ist der Text? Wie viele Kreise passen auf die Seite? – Darum ist es wichtig, dass du weisst, wie das geht.
+
Addition
-
Subtraktion
*
Multiplikation
/
Division mit Kommastellen
//
Division mit ganzzahligem Resultat
%
Modulo (Rest der Division)
Es gilt wie damals in der Schule «Punkt vor Strich».
print(2 * 3 + 5) # 11
print(2 * (3 + 5)) # 16
print(12 - 6 / 2) # 9
print((12 - 6) / 2) # 3
Wichtig ist die Unterscheidung von /
und //
. Eine Division mit einzelnem Schrägstrich ergibt immer Resultate mit Kommastellen, auch wenn das Resultat eigentlich eine ganze Zahl wäre.
print(12 / 4) # 3.0
Eine Division mit zwei Schrägstrichen ergibt immer eine ganze Zahl. Wenn die Division nicht aufgeht, wird abgerundet.
print(12 / 5) # 2.4
print(12 // 5) # 2
Modulo ergibt den Rest einer Divison.
print(12 % 5) # 2
Modulo kann sehr nützlich sein. Du benutzt es im Kopf jeden Tag, wenn du die Tageszeit in Zahlen von 1–12 umrechnest: 18 Uhr ist 18 % 12 = 6
– sechs Uhr Nachmittags.
Einige Übungen zum kennenzulernen
Als erstes musst du wissen, dass DrawBot über eine grosse Anzahl eingebauter Funktionen verfügt, die zur Ausgabe von graphischen Elementen dient, z.B. newPage()
und oval()
.
newPage(400, 400)
oval(0, 0, 100, 100)
Ein Kreis
In die Funktion oval(a, b, c, d)
verschiedene Zahlenwerte (jeweils 4) einfügen, ausführen und beobachten, was sich ändert.
Wofür stehen die Werte in den Klammern?
Beachte, dass der Nullpunkt des Koordinatensystems in DrawBot unten links ist (bei Websites ist er oben links).
Die Masseinheit in DrawBot wird erst relevant, wenn du etwas in eine Datei speicherst. Dann entspricht eine Einheit einem Pixel (bei JPG, GIF und PNG) oder einem DTP-Punkt (Print).
Ein Viertel der Fläche
newPage(400, 400)
print height(), width()
oval(0, 0, width(), height())
- was machen die Funktionen
height()
undwidth()
? - Zeichne einen Kreis ins rechte obere Seitenviertel.
Es ist eine gute Angewohnheit, dir die Werte von Funktionen oder Variablen in der Konsole anzeigen zu lassen. Das hilft zu vestehen, was da genau läuft.
Lösung
newPage(400, 400)
oval(width()/2, height()/2, 200, 200)
oder
newPage(400, 400)
oval(width()/2, height()/2, width()/2, height()/2)
Einmitten in der Fläche
- für jedes Argument in der Funktion
oval()
eine Variable schreiben: X-Position, Y-Position, und Radius - Argumente gegen Variablen tauschen
- Findest du eine Möglichkeit, das Kreiszentrum in die Mitte der Seite zu zeichnen?
- Probier es zuerst selbst, die Lösung steht im nächsten Abschnitt.
Es ist etwas umständlich, aber harmlos:
newPage(400, 400)
x_pos = height()/2
y_pos = width()/2
rad = 50 # Achtung, die oval-Funktion will Durchmesser, nicht Radius
oval(x_pos - rad, y_pos - rad, 2 * rad, 2 * rad)
Random (Zufallsgenerator)
In Python kannst du zufällige Zahlenwerte generieren.
random()
generiert zufällige Zahlen zwischen 0 und 1.randint(a, b)
generiert ganze Zahlen zwischen a und b
Versuch mit Zufallszahlen
Schreibe ein Programm, das ein Kreis zufälliger Grösse an zufälliger Position auf eine A4-Seite zeichnet.
Es wird ein exemplarisches Problem auftreten, darum schauen wir uns gleich auch mal an, wie man vorgehen könnte, wenn Fehlermeldungen auftreten.
newPage('A4Landscape')
# Durchmesser
d = randint(10, width())
x = randint(0, width())
y = randint(0, height())
oval(x, y, d, d)
Der Kreis soll bitte nicht über den rechten Seitenrand reichen.
newPage('A4Landscape')
# Durchmesser
d = randint(10, width())
x = randint(0, width() - d)
y = randint(0, height() - d)
oval(x, y, d, d)
Error
Wenn du das Programm einige Male ausführst, kommt es zu Fehlermeldungen.
Traceback (most recent call last):
File "<untitled>", line 7, in <module>
File "random.pyc", line 242, in randint
File "random.pyc", line 218, in randrange
ValueError: empty range for randrange() (0,-75, -75)
- Der Fehler passiert auf Zeile 7.
- Es gibt ein Problem mit der Funktion
randint()
Debugging
- Wir printen
height()
und die Variable d in die DrawBot-Konsole. - Die Fehlermeldung erscheint immer dann, wenn d grösser wird als
height()
Ansätze zur Lösung
Wir berechnen den Durchmesser als Wert zwischen 10 und der Seitenbreite, aber die Seite ist ein Querformat (A4Landscape). Der Fehler tritt auf, wenn das erste Argument von randint()
grösser ist als das Zweite.
Eine Lösung wäre, den Kreisdurchmesser von height()
abhängig zu machen, anstatt von width()
. Das funktioniert aber nur solange, bis wir der Funktion newPage()
ein Hochformat als Argument geben.
Lösung
Die Python-Funktionen min(a, b)
und max(a, b)
liefern den Tieferen, respektive den Höheren von zwei Werten.
newPage('A4')
# der Durchmesser soll nicht breiter/höher als die Seite werden
maxValue = min(width(), height())
d = randint(10, maxValue)
x = randint(0, width() - d)
y = randint(0, height() - d)
oval(x, y, d, d)
Der hier beschriebene Ablauf ist typisch für Problemstellungen, die beim Programmieren auftreten können: Ein Plan geht nicht so auf, wie gedacht – es treten unerwartete Fehler auf. Eine Lösung sollte den Fehler komplett zum Verschwinden bringen, damit er später nicht in einer Variante wieder auftreten kann (wenn z.B. das Ausgabeformat wechselt).
Output in Datei schreiben
Mit der DrawBot-Funktion saveImage()
lässt sich der Output in eine oder mehrere Dateien schreiben.
# als PDF speichern
saveImage('~/Desktop/datei.pdf')
Das Argument der saveImage()
Funktion ist eine Zeichenkette, also braucht es Anführungszeichen. Es handelt sich dabei um einen Pfad in Unix-Schreibweise. Schrägstriche stehen für Ordner, die Tilde ~
ist ein Kürzel für dein Home-Verzeichnis.
Zeitstempel
Du kannst zu Beginn deines Programms die Funktion strftime()
aus dem Time-Modul importieren.
from time import strftime
Mit dieser Funktion kannst du einen Zeitstempel generieren, den du an einen Dateinamen anhängen kannst. So werden die Dateien, die du generierst, nicht jedes mal überschrieben, wenn du das Programm erneut ausführst
# Zeitstempel generieren
timestr = strftime("%Y%m%d-%H%M%S")
print(timestr)
# Zeitstempel in Dateinamen einfügen
saveImage("~/Desktop/name_" + timestr + ".jpg")
Das Argument "%Y%m%d-%H%M%S"
ist ein Template für die Formatierung des Datums.
Loops
Eine Übung zum Thema Loops: Mit dem Programm, das einen variablen Kreis auf eine Seite zeichnet, produzieren wir ein hundertseitiges Dokument.
Beachten:
-
Doppelpunkt nach dem
for … in
Statement. -
Den Abschnitt nach dem
for … in
Statement einrücken.for n in range(100):
newPage('A4')
# Kreis zeichnen etc.saveImage('~/Desktop/datei.pdf')
Ändere den Code in der Schleife so …
- dass auf jeder Seite 100 Kreise gezeichnet werden.
- dass jede Seite eine andere Hintergrundfarbe hat.
Hintergrundfarbe z.B. so:
r = random()
g = random()
b = random()
with savedState:
fill(r, g, b)
rect(0, 0, width(), height())
Die Zeile with savedState():
bewirkt, dass die Einstellungen im nachfolgenden Block (z.B. Farbe oder Schriftgrösse) danach wieder zurückgesetzt werden.
Anordnung im Raster
newPage(1000, 1000)
# Durchmesser Rasterzelle
d = 100
for yFactor in range(10):
# Der «äussere Loop» berechnet den Wert zur
# Positionierung auf der vertikalen Achse und
# enthält den Loop, der die Zeilen zeichnet.
yPos = yFactor * d
for xFactor in range(10):
# Der «innere Loop» berechnet den Wert zur
# Positionierung auf der horizontalen Achse.
xPos = xFactor * d
# «Hilfslinien» zeichnen
with savedState():
fill(None)
stroke(0)
strokeWidth(1)
rect(xPos, yPos, d, d)
# Ab hier kannst einzelnen Zellen zeichnen.
# Die Zelle reicht von xPos bis xPos + d
# bzw. von yPos bis yPos + d
randDia = random() * d
oval(xPos + (d - randDia)/2, yPos +(d - randDia)/2, randDia, randDia)
Eigene Funktionen schreiben
Du kennst schon einige Funktionen aus der Sprache Python, z.B. print()
, random()
und range()
; und solche, die es nur in DrawBot gibt, z.B. oval()
und newPage()
.
Es gibt zwei Sorten von Funktionen:
- Funktionen mit Nebenwirkung (engl. side effect)
- Funktionen, die einen Wert liefern (engl. return)
Eine Nebenwirkung ist etwas, was sich auf den Output des Programms auswirkt, z.B. Ausgabe von Text oder Zeichnen auf die Fläche.
Funktionen, die einen Wert liefern, sind z.B. width()
oder random()
die bei Aufruf einen Wert zurückgeben, den du in deinem Programm weiterverarbeiten kannst.
Du kannst deine eigenen Funktionen schreiben. Zuerst eine mit Nebenwirkung:
def sayHi():
print("Hi")
sayHi()
Argmumente sind Werte, die einer Funktion übergeben werden und die darin als Variablen verwertet werden können.
def sayHi(name):
print("Hi", name)
sayHi("Bibi")
Du kannst Funktionen verwenden, um Teile eines Programms, die kompliziert sind, zu vereinfachen. Oer um Abschnitte, die sich mehrmals wiederholen, zusammenzufassen.
Beispiel: Eine Funktion, die einen eingemitteten Kreis zeichnet.
Das Einmitten eines Kreises ist in Drawbot immer etwas mühsam, weil ihr Ankerpunkt an der linken unteren Ecke des (gedachten) Quadrats liegt, das ihn enthält. Mit einer Funktion kannst du das stark vereinfachen.
def centeredCircle(x, y, radius):
oval(x - radius, y - radius, 2 * radius, 2 * radius)
newPage(100, 100)
centeredCircle(50, 50, 40)
Return
Eine Funktion, die einen Wert liefert, statt etwas zu zeichnen oder zu schreiben, kann z.B. helfen, eine Rechnung zu vereinfachen.
Dazu schreibst du return
am Ende der Funktionsdefinition, gefolgt von der Rechnung, dessen Resultat die Funktion liefern soll.
def durchschnitt(a, b):
return (a + b) / 2
print(durchschnitt(3, 7))
print(durchschnitt(23, 37))
translate() und rotate()
Die Funktion translate()
verschiebt den Nullpunkt, d.h. das ganze Koordinatensystem.
# Den Nullpunkt um 500 Einheiten nach rechts oben verschieben
translate(500, 500)
rotate()
dreht das Koordinatensystem um einen bestimmten Winkel. Optional kann zusätzlich zum Winkel ein Punkt angegeben werden, um welches das Koordinatensystem gedreht werden soll.
# Das Koordinatensystem im Gegenuhrzeigersinn um 45° um die Mitte
# einer 1000 mal 1000 Einheiten grossen Fläche drehen.
rotate(45, center=(500, 500))
Beachten: In diesem Beispiel wird um den Mittelpunkt gedreht, dadurch verschiebt sich der Nullpunkt des Koordinatensystems weg von der unteren linken Ecke der sichtbaren Fläche!
Sowohl bei translate()
als auch bei rotate()
empfiehlt es sich, den ursprünglichen Zustand des Koordinatensystems mit savedState()
zu speichern und wiederherzustellen.
savedState()
Mit der Funktion savedState()
kannst du in einem Abschnitt deines Programms Einstellungen wie Farbe und Zustand des Koordinatensystems ändern und danach die ursprünglichen Einstellungen wiederherstellen.
newPage(1000, 1000)
fill(1, 0, 0)
with savedState():
fill(0, 0, 1)
translate(0, 200)
rect(0, 0, 100, 100)
rect(0, 0, 100, 100)