Zeichnen mit Pottis und Partikel Effekt p5.js
In diesem Tutorial lernst du wie in p5.js mithilfe von Arduino und Potentiometer (kurz Poti) zeichnen kannst.
Inspiriert wurden wir von den Zaubertafel (Etch a Sketch) bei denen man zwei Knöpfe zum drehen hatte, wobei einer die X-Richtung und der andere die Y-Richtung gesteuert hat, um dann eine Zeichnung zu erschaffen.
Aufbau
Um unseren Aufbau nachzubauen, braucht ihr den Arduino, ein Steckbrett, zwei Potis und verschieden lange Kabel. Bei beiden Potis werden auf der einen Seite mit plus und minus verbunden, welche in GND und 5V plaziert werden. Die andere Seite der Poti, der Mittelpin, wird einmal mit A0 und A1 verbunden.

Vorbereitungen
Wir haben unseren Code in p5.js Web Editor aufgebaut, damit wir gleich eine Zeichenfläche zum visualieren haben. Damit der Web Editor auch die Werte der Potis auslesen kann, müssen wir noch einen Zwischenschritt machen. Dafür brauchen wir erstmal das Programm Arduino IDE (https://www.arduino.cc/en/software/) in welches wir einen kleinen Code schreiben und hochladen. Den Code kannst du dir auch im Programm selber herholen über File -> Examples -> 01.Basics -> AnalogReadSerial, dieser gilt dann aber nur für beispielsweise einen Potti und nicht für zwei, deshalb haben wir den unteren Code für beide Pottis erstellt. Sobald wir den Code haben können wir diesen auf unseren Arduino hochladen.
//Setup Funktion läuft einmal, wenn man den Reset-Knopf drückt
void setup() {
//Startet serielle Kommunikation
Serial.begin(9600);
}
//Loop Funktion läuft un Endlosschleife
void loop() {
int pot1 = analogRead(A0); //Wert von Poti 1, Variablen werden später noch gebraucht
int pot2 = analogRead(A1); //Wert von Poti 2, Variablen werden später noch gebraucht
//Werte Seriell senden
Serial.print(pot1);
Serial.print(",");
Serial.println(pot2);
delay(20); //kurze Pause in Milisekunden
}
Da aber dieses Programm keine Verbindung zu dem Web Editor p5.js aufbauen kann, müssen wir uns noch mit dem Programm SerialControl verbinden, welches man bei GitHub (https://github.com/p5-serial/p5.serialcontrol/releases )herunterladen kann. Zunächst benötigen wir unseren Port, dieser wird im Arduino IDE angezeigt, kann aber auch in SerialControl ausgewählt werden. Anschließend öffnen wir den Port und lassen und in der Serial Console die Werte ausgeben, dafür wählen wir "console enabled" und "read in ASCII" aus. Nun solltest du zwei Werte angezeigt bekommen, welche sich ändern sobald man die Potis dreht.
Wichtig ist das Programm im Hintergrund weiter laufen zu lassen.

p5.js
Schlussendlich können wir auf die Editor Seite vom p5.js (https://editor.p5js.org) gehen. Dort sind im HTML Dokuement die nötigen Links bereits eingebunden um eine Verbindung zu Serial Control herzustellen. Es gibt auch die Möglichkeit diese lokas zu verlinken, da
Hier ist es noch wichtig das wir in die HTML Datei Links aus dem SerialControl einfügen, dabei ist die Reihenfolge wichtig. Man kann auch die Library lokal einbinden. In der sketch.js Datei schreiben wir jetzt unseren eigentlichen Code.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/addons/p5.sound.min.js"></script>
<script language="javascript" type="text/javascript" src="https://cdn.jsdelivr.net/npm/p5.serialserver@0.0.28/lib/p5.serialport.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />
</head>
<body>
<main>
</main>
<script src="sketch.js"></script>
</body>
</html>
Schritt 1: Variablen
let serial;
let pot1 = 0, pot2 = 0; //Speichert die Poti-Werte (A0 = X, A1 = Y)
let pottys = [];
Dabei ist serial
für die Kommunikation mit dem Arduino zuständig. Die pot1
, pot2
speichern die Werte von den Potis, welche wir zuvor in dem Programm Arduino IDE festgelegt haben. Dabei ist der Potti der an A0 angeschlossen ist für die X-Achsenrichtung und A1 für die Y-Achsenrichtung zuständig. Der Array pottys
speichert alle Linien/Effekte die im aktuellen Frame gezeichnet werden sollen.
Schritt 2: Zeichenfläche
function setup() {
createCanvas(windowWidth, windowHeight);
colorMode(HSB, 360, 200, 100, 255);
noStroke();
// Serialport initialisieren
serial = new p5.SerialPort();
serial.on('data', serialEvent);
serial.on('open', () => console.log("Port offen!"));
serial.open("COM7");
}
Die function setup
dient dazu die Voreinstellungen zu initialisieren. Hier bestimmen wir Grundlagen wie die Zeichenfläche und die Verbindung zum Arduino.
Mit createCanvas
stellen wir die Größe der Zeichenfläche auf die Höhe und Breite des Browserfensters ein, der colorMode
wird hier im HSB-Farbraum dargestellt, dabei stehen die 360
für den Farbton (Hue) schwarz, die 200
stehen für die Sättigung (Saturation), die 100
für die Helligkeit (Brightness) und die 255
für die Transparanz.
noStroke()
entfernt die Umrisslinien der Zeichenfläche.
Anschliesßend wird der Arduino mit den verschiedenen Serial Komponenten mit eingebunden, hierbei ist wichtig das wir den Port in serial.open
angeben. In unserem Fall ist das ("COM7")
da wir mit einem Windows Pc gearbeitet haben, falls ihr mir einem Macbook arbeitet kann dieser Port beispielsweise so aussehen serial.open("/dev/tty.usbmodem13101").
Schritt 3: Zeichenschleife
function draw() {
background(0, 0, 0, 80); // Trail-Effekt durch Transparenz
let potX = map(pot1, 0, 1023, 0, width);
let potY = map(pot2, 0, 1023, 0, height);
pottys.push(new Potty(potX, potY, 'vertical'));
pottys.push(new Potty(potX, potY, 'horizontal'));
for (let i = pottys.length - 1; i >= 0; i--) {
let p = pottys[i];
p.update();
p.display();
if (p.isGone()) {
pottys.splice(i, 1);
}
}
}
Die funtion draw()
dient zur Aktualisierung des Bildschirms.
Der background
wird auf halbtransparent schwarz gesetzt, um ein Nachleuchten des Cursers zu kreieren, dadruch entsteht ein Trail-Effekt bzw. Verblendungseffekt.
Durch das Mapping der zwei Potis potX
und potY
wird bestimmt das der Curser minimal bei Wert 0 und maximal bei Wert 1023 verlaufen kann. Der Wert 1023 kommt aus dem Programm SerialControl da das die Maximalen Werte von den bei uns verwendeten Potis sind.
Durch potty.push
werden immer wieder neue Partikel hinzugefügt, damit es in einer durchgehenden zufälligen Bewegung bleibt und das in vertical
Y-Achse und horitontal
X-Achse.
Die in der for-Schleife enthaltenen p.update
und p.display
dienen dazu die Partikel des Cursers ständig zu aktualisieren und immer etwas auf der Zeichenfläche zu projezieren. Die folgenden Funktionen p.isGone
und pottys.splice
sorgen für Ordnung und überprüfen ob die Partikel noch gebraucht werden oder irrelevant geworden sind und hilft Speicher zu sparen.
Schritt 4: Daten Empfang
function serialEvent() {
let data = serial.readLine();
if (data) {
let werte = split(data, ',');
if (werte.length >= 2) {
pot1 = int(werte[0]); //X-Richtung
pot2 = int(werte[1]); //Y-Richtung
}
}
}
Die Funktion serialEvent()
wird in p5.js verwendet, um die Daten automatisch von Serial Control oder direkt vom Arduino zu empfangen. Die let data = serial.readLine()
liest die Daten dann ein, dabei ist data
ein String der die Werte wie beispielsweise "512,300" annimmt, das sind die Werte die in dem Programm SerialControl angezeigt werden. Die angrenzende if
- Schleife überprüft ob tatsächlich auch Daten empfangen werden. Die split(data, ',')
teilt die ankommenden Werte dann bei jedem Komma auf, um die werte[0]
und werte[1]
abgrenzen zu können. Dabei überprüft die werte.length <= 2
das auch zwei Werte empfangen werden, anschließend werden in den Variablen pot1
und pot2
diese Ganzzahlwerte gespeichert.
Schritt 5: Partikel Klasse
class Potty {
constructor(x, y, direction) {
this.pos = createVector(x, y); //Position wird mit Koordinaten x, y erstellt
this.direction = direction; //Richtung horizontal oder vertical
if (direction === 'vertical') { //wenn Vertical
let dir = random([1, -1]);
this.vel = createVector(0, dir * random(1, 2));
}
else { //wenn nicht vertical
let dir = random([1, -1]);
this.vel = createVector(dir * random(1, 2), 0);
}
this.lifespan = 255;
this.size = random(10, 30);
this.hue = random(0, 360);
this.rotationSpeed = random(-0.05, 0.05);
}
update() {
this.vel.rotate(this.rotationSpeed);
this.pos.add(this.vel);
this.lifespan -= 10;
}
display() {
fill(this.hue, 80, 100, this.lifespan);
ellipse(this.pos.x, this.pos.y, this.size);
}
isGone() {
return this.lifespan <= 0;
}
}
Schlussendlich benötigen wir noch eine class Potty
welche für die kleinen, graphischen Partikel zuständig sind welche sich bewegen, verändern und irgendwann verschwinden.
Dabei initialisiert constructor(x, y, direction)
die Position (x oder y) und die Richtung (vertical oder horizontal) der Partikel. Die Methoden this.pos
und this.direction
speichern dann die Position und Richtung der Partikel.
In der if-else-Schleife wird die Geschwindigkeit und die Richtung der Partikel zufällig erzeugt.
Wenn die generelle Richtung vertikal ist, dient dir = random([1, -1])
zu zufällig zu entschieden ob sich die Partikel nach oben oder nach unten bewegen und createVector(0, dir * random(1, 2))
gibt die Geschwindigkeit auf der Y-Achse an. Wenn die allgemeine Richtung horizontal ist, benötigen wir die gleichen Funktionen um die Richtung (oben/unten) und Geschwindigkeit zufllig zu bestimmen.
this.lifespan = 255
entscheidet über die Lebensdauer der Partikel, dieser Wert wird mit der Zeit reduziert, deswegen verschwinden die Partikel dann.
this.size = random(10, 30)
generiert eine zufällige Größe zwischen 10 und 30 Pixel.
this.hue = random(0, 360)
entscheidet über den Farbtons des Partikels aus einem Farbspektrum wobei 0
für Rot und 360
wieder für Rot steht.
this.rotationSpeed = random(-0.05, 0.05)
bestimmt die Rotationsgeschwindigkeit des Partikels.
Mithilfe von update()
werden die Eigenschaften der Partikel gefestigt und aktualisiert also die Bewegung (this.vel.rotate(this.rotationSpeed)
) , Drehung ( this.pos.add(this.vel)
)und Lebenszeit( this.lifespan -= 10
).
display()
ist dazu da die Partikel zu zeichnen, wobei bei fill()
mit this.hue
der Farbton, 80
die Sättigung, 100
die Helligkeit und this.lifespan
die Lebensdauer ist.
ellipse(this.pos.x, this.pos.y, this.size)
zeichnet einen Kreis/Elipse an Postion this.pos
mit dem Durchmesser this.size
.
Zu letzt wird mit isGone()
geprüft ob die Lebensdauer aufgebraucht ist und anschließend aus dem Array gelöscht wird.
Quellenangaben
Hier kommst du direkt zum Code im Web Editor p5.js.
Unsere Inspiration findet ihr hier und hier.
Zum einrichten von Serial Control hatten wir hilfe von hier und hier.
Viel Spaß beim Nachmachen und Ausprobieren.