//

P5.JS: Zeichnen mit der Open-Source-JavaScript-Bibliothek

28.11.2022 | 13 Minuten Lesezeit

Im Rahmen eines kleinen Projekts, bei dem es um das Thema Berechnung von Flugrouten ging, brauchten wir eine einfache und leichtgewichtige Möglichkeit, die Route und andere Bereiche auf der Karte zu visualisieren. Bei der Suche nach einem passenden Framework sind wir auf einen YouTube-Kanal gestoßen. Auf dem Kanal TheCodingTrain veröffentlicht der Mathematiker Daniel Shiffman spannende Videos, in denen er mathematische Probleme und deren Lösungen erklärt und diese mit einem JavaScript-Framework visuell umsetzt. Das Framework, das er nutzt, heißt P5.JS. Und genau das haben wir uns für das oben genannte Projekt angeschaut. Was dabei herausgekommen ist und ob das Framework wirklich für diesen Anwendungsfall geeignet ist, schauen wir uns im Folgenden an.

Einrichtung

Zu Beginn steht wie immer die Installation und Konfiguration des Frameworks. Hierfür stehen uns drei Wege zur Verfügung, die allesamt keiner großen Konfiguration bedürfen, was dieses Framework umso reizvoller macht.

Der Webeditor

Die beste Nachricht zuerst: Wer nichts einrichten und ohne große Hürden einfach losarbeiten will, dem lege ich den P5.JS Editor ans Herz. Hier habt ihr die Möglichkeit, in eurem Webbrowser ohne Anmeldung eure erste kleine oder auch große Anwendung zu bauen.

Als Beispiel habe ich mal einen kleinen Smiley in der codecentric-Farbe gebaut. (Beispielcode befindet sich unter dem Bild)

1function setup() {
2  createCanvas(300, 300);
3}
4
5function draw() {
6  background(220);
7  
8  // face
9  fill(135,255,197)
10  ellipse(150, 150 ,300)
11  
12  // color for the next shapes
13  fill(0,0,0)
14  
15  // eyes
16  ellipse(100, 100 ,30
17  ellipse(200, 100 ,30)
18  
19  // mouth
20  square(100,190,10)
21  square(190,190,10)
22  rect(110,200,30,10)
23  rect(130,210,40,10)
24  rect(160,200,30,10)
25}

IDE

Ihr könnt P5.JS aber auch in der IDE eurer Wahl benutzen. Hierzu lässt sich P5.JS zum Beispiel über ein Content Delivery Network einbinden. Hierzu einfach den Script-Tag in die index.html einbinden. Den jeweils passenden findet ihr auf der CDNJS Seite für P5.JS. Hier ein Beispiel in der aktuellen Version:

1<script 
2    src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.min.js">
3</script>

Oder ihr ladet euch die Bibliothek von der offiziellen P5.JS Webseite runter. Dort findet ihr auch alle weiteren Informationen rund um die Einrichtung von P5.JS. Zusätzlich gibt es auch die Möglichkeit, einen Paketmanager zu nutzen. Alle Informationen dazu findet ihr auf der npmjs-Seite.

Nun seid ihr fertig und könnt mit der Nutzung von P5.JS beginnen.

Grundlagen

Die grundlegenden Funktionen und Funktionsweisen sind schnell erklärt. Zunächst einmal teilt sich die Anwendung in die zwei Grundbausteine setup() und draw().

1function setup() {
2}
3
4function draw() {
5}

In der Setup-Funktion bestimmt ihr die grundlegenden und vor allem statischen Elemente eurer Anwendung. Alles, was hier implementiert ist, wird nur einmal nach dem Start der Anwendung ausgeführt.

Die Draw-Funktion läuft hingegen als Loop ab, was euch die Möglichkeit gibt, die Anwendung während der Laufzeit zu verändern. Diese Funktion läuft also in einer Dauerschleife, und zwar so lange, bis das Programm beendet wird oder ihr mittels noLoop() das Ganze beendet.

Ein paar wichtige Hinweise:

  • setup() und draw() darf es nur einmal in eurem Programm geben
  • draw() wird automatisch aufgerufen und braucht nicht in der setup() implementiert werden.
  • Wird noLoop() in der setup() implementiert, dann wird die draw() dennoch einmal ausgeführt.

Nach dem Loop ist vor dem Loop!

Wenn ihr mit der Verwendung von P5.JS startet, wird euch schon bald das seltsame Verhalten eurer Anwendung auffallen. Zum Beispiel habt ihr ein paar Geometrien gezeichnet und wolltet eine davon grün färben. Leider sind danach alle anderen auch grün. Auch die clear() -Funktion, die euch die Zeichenfläche wieder aufräumt, sorgt manchmal dafür, dass man denkt, die Anwendung funktioniere nicht.

Nehmen wir mal als Beispiel die folgende draw()-Funktion:

1function draw () {
2    rect(10, 10, 50, 50);
3    fill(255, 204, 0);
4    rect(70, 10, 50, 50);
5}

Wir zeichnen also als Erstes ein Rechteck und sagen dann, dass alles, was danach folgt, gelb eingefärbt sein soll. Danach zeichnen wir noch ein Rechteck. Was wir erwarten, ist Folgendes:

Was wir erhalten, ist aber etwas anderes:

Warum ist das so? Die draw()-Funktion läuft in einem Loop. Das heißt, im ersten Durchlauf erhalten wir tatsächlich das gewünschte Ergebnis. Jedoch haben wir am Ende des ersten Durchgangs auch gesagt, ab hier alles gelb. Das heißt, der nächste Durchgang startet mit genau dieser Information. Wie können wir jetzt aber unser Ergebnis erzeugen? Na ja, wir sagen explizit, dass unser erstes Rechteck keine Füllung haben soll.

1function draw () {  
2    noFill()  
3    rect(10, 10, 50, 50);  
4    fill(255, 204, 0);  
5    rect(70, 10, 50, 50);  
6}

Wenn man das einmal verinnerlicht hat, kann nicht mehr viel schiefgehen.

Weitere Grundbausteine

Neben diesen beiden Funktionen, die ihr immer braucht, gibt es auch weitere, die schnell den Einzug in euer Programm halten werden.

preload()

Wenn ihr vor dem Start der Anwendung Daten laden wollt, ist die Preload-Funktion genau die richtige Stelle. Diese Funktion ist von Hause aus asynchron und ruft selbstständig die Setup-Funktion auf. Das ist echt super, wenn ihr Daten von einem Server oder eine große Menge an Daten vom Rechner ladet. Erst, wenn diese Daten komplett geladen sind, wird die Setup-Funktion aufgerufen. Dies entspricht also dem async-await Verhalten.

Zudem gibt es hier von P5.JS einige Methoden, die euch Daten asynchron laden. Ihr braucht dafür also keine zusätzliche Bibliothek. Hierbei sind vor allem die loadJSON(), loadImage() und loadStrings() zu erwähnen. Die Funktionen bieten die Möglichkeit, Daten sowohl vom Rechner als auch über eine URL zu laden.

Hier noch ein kleines Beispiel, wie der Preload über eine URL bzw. den lokalen Speicher aussieht:

1function preload () {  
2    dataFromAnAPI = loadJSON('http://localhost:3000/getZones');
3    dataFromLocal = loadJSON('../../resources/example.geojson');    
4}

mousePressed(), mouseReleased(), …

Da wir ja nicht nur statische Anwendungen bauen wollen, brauchen wir auch Funktionen, die uns Interaktionen ermöglichen. Mit den Funktionen: mousePressed() und mouseReleased() lässt sich wunderbar die Interaktion mit der Maus einfügen.

In der oben genannten Routing-Anwendung konnten wir diese beiden Funktionen sehr gut nutzen, um zu unterscheiden, ob der Nutzer nur einen einfachen Klick macht oder mit gedrückter Maustaste die Karte bewegt. Hierfür haben wir einfach beim mousePressed() einen Zeitstempel gesetzt. In der mouseReleased() haben wir daraus dann das Delta gebildet und entschieden, dass alles, was länger als 200ms dauert, ignoriert wird. So lässt sich mit einem einfachen Klick ein Punkt zeichnen und bei gedrückter Maustaste die Karte bewegen, ohne einen Punkt zu zeichnen.

1function mousePressed() {  
2    down = Date.now();  
3}  
4  
5function mouseReleased() {  
6    if ((timeTaken = Date.now() - down) < 200) {  
7        ellipse(mouseX, mouseY, 10, 10);  
8    }  
9}

Im Beispiel sieht man auch direkt zwei weitere Elemente: die mouseX- und mouseY-Attribute. Diese geben euch immer die X- und Y-Position euerer Maus in Bezug auf eure Zeichenfläche wieder.

createCanvas()

Um überhaupt zeichnen zu können, brauchen wir eine Zeichenfläche (Canvas), die ihr euch mit der Funktion createCanvas() erzeugen könnt. Diese bekommt in der Regel zwei Parameter übergeben: die Breite und die Höhe der Zeichenfläche in Pixel. Ihr habt auch die Möglichkeit, euch die Auflösung eures Bildschirms auszugeben. Damit lässt sich eine Art FullScreen erzeugen.

1createCanvas(600, 600)
2createCanvas(displayWidth, displayHeight)

Ein paar wichtige Hinweise:

  • Es darf immer nur eine createCanvas-Funktion geben.
  • Diese gehört immer in die Setup-Funktion.
  • Wird diese nicht gesetzt, erzeugt P5.JS diese automatisch mit 100 × 100px.
  • Die Koordinate (0,0) liegt immer oben links in der Ecke.

Linien, Formen, Farben, Polygone

Jetzt habt ihr alle wichtigen Grundlagen an der Hand und könnt mit dem Zeichnen beginnen. Hierfür stellt euch P5.JS einen ganzen Werkzeugkasten mit Möglichkeiten zur Verfügung. Diesen findet ihr unter den Referenzen auf der P5.JS Seite.

Wir schauen uns nun ein paar dieser Elemente anhand des schon erwähnten Routing Projekts an.

Im folgenden Bild seht ihr den aktuellen Arbeitsstand, der eine große Menge an Inhalt bietet, den wir uns anschauen werden. Wie hier zu sehen, haben wir Linien mit Start und Endpunkten (Kreise), Rechtecke mit gestrichelter Linie und Polygone, die eine leicht transparente Farbe besitzen.

Linien und primitive Geometrien

Die einfachste Form ist ein Punkt. Dieser bekommt lediglich die X- und Y-Koordinaten.

1point(x, y)

Danach folgen der Kreis und das Rechteck. Bei beiden gibt es zwei Varianten: nämlich die allgemeine und die spezielle Variante.

Beim Kreis haben wir die ellipse() und die circle()-Funktionen. Beide bekommen die X- und Y-Koordinaten sowie eine Breite bzw. den Durchmesser. Bei der Ellipse lässt sich optional noch die Höhe angeben, was sie bei unterschiedliche Parametern erst zur Ellipse macht.

Da die Höhe bei der Ellipse optional ist und somit beim Weglassen den Wert der Breite annimmt, nutzt man in der Regel auch für einen Kreis die Ellipse. Die Breite ist also äquivalent zum Durchmesser.

1circle(x, y, d)
2ellipse(x, y, w, [h])

Das gleiche Verhalten haben wir auch bei der Erzeugung eines Rechteckes. Auch hier gibt es eine allgemeine sowie eine spezielle Variante, bei der man ebenfalls eher auf die allgemeine zurückgreift.

Mit square() lässt sich also ein Quadrat erzeugen und mit rect() ein Rechteck. Bei beiden wird der linke obere Punkt als Startkoordinate angegeben und danach erfolgt die Seitenlänge bzw. Breite und optionale Höhe.

1square(x, y, s)
2rect(x, y, w, [h])

Zu guter Letzt haben wir noch die line(). Hier wird einfach die X- und Y-Koordinaten für den Start und den Endpunkt eingegeben.

1line(x1, y1, x2, y2)

Um eine Linie als gestrichelt oder gepunktet zu gestalten, gibt es die Option setLineDash(), welche jedoch eine native Funktion des CanvasRenderingContext2D Frameworks ist und nicht von P5.JS stammt. Dieser Funktion könnt ihr ein Array übergeben, in der Ihr definiert, welche Länge die einzelnen Abschnitte haben sollen. Zum Beispiel würdet ihr eine Linie mit 10 Einheiten Linie und 10 Einheiten Lücke wie folgt definieren:

1// --  --  --  --  --
2drawingContext.setLineDash([10,10]);

Diese kann beliebig erweitert werden, indem ihr weitere Parameter setzt. Eine leere Liste sorgt dann dafür, dass ihr eine durchgezogene Linie erhaltet.

1// - --  - --  - --  -
2setLineDash([5, 5, 10, 10]); 
3
4// -----------
5setLineDash([]);

Farben

Farben lassen sich mit P5.JS ebenfalls sehr einfach einsetzen. Hierfür steht euch eine kleine Auswahl an Optionen zur Verfügung.

Zunächst erzeugt ihr die Farbe immer mit der Funktion color(). In dieser definiert ihr eure Farbe. Dies kann als RGB(A)-, SVG- bzw. CSS-Farbnamen, HEX, HSL(A) und HSB definiert werden.

1// RGB als Array
2color(255, 204, 0);
3color('rgb(255, 204, 0)');
4
5// RGBA  als Array und als String
6color(255, 204, 0, 150);
7color('rgba(255, 204, 0, 150)');
8
9// SVG & CSS Farbnamen
10color('magenta');
11
12// HEX
13color('#0f0');
14
15// HSL
16color('hsl(160, 100%, 50%)');
17color('hsla(160, 100%, 50%, 0.5)');

Dies definiert aber zunächst nur die Farbe an sich, wird aber noch keinem Element zugewiesen. Im Prinzip lässt sich fast jedem Element eine Farbe zuordnen. So könnt ihr euren Linien mit der stroke()-Funktion eine Farbe (und auch eine Linienstärke) zuweisen.

1stroke(color(255, 204, 0));
2line(30, 20, 85, 75);

Neben* stroke() *gibt es auch noch die fill()-Funktion. Diese füllt euch Formen und Bereiche. Wichtig: Ihr könnt damit keine Linien und Punkte einfärben. Dies wird am besten am Beispiel des Rechteckes deutlich:

1stroke(0, 0, 255);  
2strokeWeight(5);  
3fill(255, 204, 0);  
4rect(30, 20, 85, 75);

Hier haben wir ein Rechteck mit einer gelben Füllung und einem blauen Rahmen. Wie ihr an diesem Beispiel sieht, gibt es auch Funktionen, in der ihr direkt die Farbe als Parameter schreiben könnt. Ihr braucht sie also nicht extra mit *color() *definieren.

Polygone

Wir haben ja jetzt eigentlich alles an der Hand, um die gewünschten Anforderungen aus dem Routing-Projekt zu erstellen. Das einzige, was noch fehlt, sind Polygone, die mit einer leicht transparenten Farbe erzeugt werden sollen. Na ja, wir müssen doch nur Linien zeichnen, um ein Polygon zu erzeugen, und wie man Elemente einfärbt, wissen wir auch. Mit dem folgenden Beispiel sollte das Ganze funktionieren:

1fill(200, 100, 100, 150);  
2line(10, 10, 60, 10);  
3line(60, 10, 70, 80);  
4line(70, 80, 10, 60);  
5line(10, 60, 10, 10);

Das Ergebnis entspricht zwar der korrekten Form, das mit der Farbe hat jedoch nicht funktioniert.

Aber warum?

Na ja wir haben einzelne Linien gezeichnet, die für das Programm aber überhaupt keinen Zusammenhang haben. Um diesen Zusammenhang herzustellen, müssen wir die beginShape() und endShape()-Funktionen einbauen. Unsere Linien packen wir also zwischen diese beiden Funktionen. Aus den Linien müssen wir nun noch Eckpunkte machen. Dies geschieht, indem wir die line() durch vertex() ersetzen und darin nur noch die Startpunkte eintragen.

1fill(200, 100, 100, 150);
2beginShape();  
3    vertex(10, 10);  
4    vertex(60, 10);  
5    vertex(70, 80);  
6    vertex(10, 60);  
7endShape(CLOSE)

Und schon passt es!

Das Quadrat, das jetzt im Hintergrund liegt, habe ich nur eingebaut, damit man sieht, dass die Farbe auch wirklich transparent ist. Zudem habe ich noch ein CLOSE in das endShape() eingefügt. Das sorgt dafür, dass das Polygon am Ende geschlossen wird. Ihr könnt auch dem beginShape()-Parameter übergeben:

  • LINES
  • POINTS
  • TRIANGLES

Diese definieren, wie die einzelnen Eckpunkte miteinander verbunden werden. Wenn ihr nichts angebt, werden einfach alle nacheinander zu einem Polygon verbunden.

Extras

Probleme mit setup(), draw(), …

Wir hatten bei uns im Projekt ein paar Schwierigkeiten bei der Nutzung von externen Bibliotheken. Das konnten wir fixen, in dem wir die P5.JS-Grundfunktionen anders aufgerufen haben.

1// Hat nicht funktioniert
2function draw(){
3...
4}
5
6// Hat funktioniert
7window.draw = function () {
8...
9}

Das lag ggf. an den Bibliotheken oder an unserem Setup. Vielleicht kommt es bei euch gar nicht zu diesem Problem, falls doch, habt ihr hier einen Lösungsansatz.

Was hat es mit Processing auf sich?

Wenn ihr euch mit P5.JS beschäftigt, werdet ihr schnell auch auf Processing stoßen, von dem P5.JS abstammt. Und hier stellt sich schnell die Frage: Warum P5.JS, wenn es schon Processing gibt? Deshalb hier mal fix ein kleiner Vergleich der beiden Bibliotheken. Am Ende findet ihr noch eine Tabelle mit der Gegenüberstellung der beiden Frameworks.

Processing ist eine Java-Bibliothek, die Funktionen zum Zeichnen, für den Umgang mit Daten, Hardware, Sound und einiges mehr bietet. Zudem gibt es hierfür einen Editor für den Desktop.

P5.JS hingegen ist eine JavaScript-Bibliothek und verfügt über einen ähnlichen Funktionsumfang wie Processing, hat aber zusätzlich Webkomponenten (HTML, CSS, DOM). Es besitzt, wie im ersten Abschnitt erwähnt, auch einen eigenen Editor, der jedoch ein webbasierter ist.

Ein elementarer Unterschied liegt in der Ausführung des Programms. Processing erzeugt eine ausführbare Desktopanwendung (Mac, Linux und Windows), wohingegen P5.JS direkt im Browser ausgeführt wird.

Die unterschiedlichen Programmiersprachen sorgen für eine unterschiedliche Syntax, weshalb ein in Processing geschriebenes Programm nicht einfach in P5.JS oder vice versa übertragbar ist.

Neben den offensichtlichen Syntax-Unterschieden gibt es auch einige Unterschiede in den Funktionsaufrufen.

Zum Beispiel wird das Erzeugen der Zeichenfläche im P5.JS mit:

1createCanvas(512, 512);

ausgeführt und im Processing mit:

1size(512, 512);
P5.JSProcessing
AnwendungsgebietWeb-AnwendungenDesktop-Anwendungen
ProgrammierspracheJavaScriptJava
GeschwindigkeitKommt bei aufwendigen Anwendungen an die Grenzen der PerformanceHohe Geschwindigkeit bei aufwendigen Anwendungen
OpenGL und 3D AnwendungenWEBGL mit three.jsP3D und P2D
DatenJSONTable (besser für große Datensätze)
HardwareIntegrierbar mit jeder JS-Bibliothek Hardwarefunktionalität bereitstellt. Z.b.:node-p5Sehr gute Anbindung an viele Hardwarekomponenten wie dem Pi oder Arduino

Karte

Wir haben hier immer mal wieder die Karte gezeigt. Diese ist kein Teil von P5.JS, sondern eine eigene Bibliothek. Hier haben wir uns für Mappa.js entschieden. Schaut da gerne mal auf die Seite, falls ihr ein Projekt umsetzen wollt, in dem ihr eine interaktive Karte braucht. Das ist in wenigen Schritte angelegt. Hier kurz unsere Implementierung:

1const mappa = new Mappa('Leaflet');  
2const options = {  
3    lat: 50.950186,  
4    lng: 11.039531,  
5    zoom: 13,  
6    style: "http://{s}.tile.osm.org/{z}/{x}/{y}.png"  
7}
8let myMap;
9
10window.setup = function () {  
11    canvas = createCanvas(displayWidth, displayHeight);  
12    myMap = mappa.tileMap(options);  
13    myMap.overlay(canvas)  
14}

Fazit

Das war jetzt eine Menge Text mit vielen Informationen. Ich hoffe, dass dabei nicht der Eindruck entstand, dass P5.JS ein aufwendiges Framework mit einer hohen Lernkurve ist. Denn genau das Gegenteil ist der Fall. Es ist im Handumdrehen aufgesetzt und startklar. Die grundlegenden Elemente sind nahezu selbsterklärend und somit einfach zu handhaben. Aber das Wichtigste: Es macht einen riesengroßen Spaß, damit zu arbeiten. Man kommt schnell zu einem Ergebnis und kann sich kreativ austoben. Wer zum Beispiel ein mathematisches Problem mal fix visualisierten will, kann das mit P5.JS in wenigen Schritten tun. Das hilft auch, wenn man Sachen erklären will. Denn ein Bild sagt mehr als 100 Zeilen JavaScript-Code.

Beitrag teilen

Gefällt mir

12

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.