Digital Typography – UI/UX Design

Technik Workshops

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() und width()?
  • 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)

Raster mit translate() und savedState()