Dynamisches Selbstportrait: Interaktive Visualisierung mit Slidern in p5.js
In unserem Tutorial zeigen wir, wie ein Selfie mithilfe von p5.js in ein generatives Partikelporträt verwandelt wird, das interaktiv auf physische Slider/Potentiometer über Arduino reagiert.
Tutorial
Für diese Anwendung brauchen wir zunächst den p5.js Web Editor.
Erstellen des p5.js Code im Web Editor
Die Grundstruktur besteht aus:
Canvas: Fläche, auf der alles gezeichnet wird
Serielle Verbindung: Daten vom Arduino empfangen
UI-Elemente: Datei-Upload, Slider und Button
Zuerst werden Variablen für Bild, Partikel, Arduino-Daten und UI-Elemente definiert.setup()
erstellt die Zeichenfläche und bereitet das UI vor.
let img;
let particles = [];
let fileInput;
let imgReady = false;
let serial;
let latestData = "512,512,512,0"; // Platzhalter für Arduino-Daten
let sizeVal = 4; // Partikelgröße
let driftVal = 0.5; // Eigenbewegung
let pushVal = 6; // Verdrängungskraft
let motionEnabled = true; // Bewegung ein/aus
let sizeSlider, driftSlider, pushSlider, motionButton;
function setup() {
createCanvas(windowWidth, windowHeight);
imageMode(CENTER);
noStroke();
textSize(14);
fill(255);
serial
öffnet die serielle Verbindung zu Arduino über COM6 und registriert eine Funktion (gotData)
für eingehende Daten und leert den Empfangspuffer.
// Serielle Verbindung öffnen
serial = new p5.SerialPort();
serial.open("COM6"); // <-- Passe hier deinen Arduino-Port an
serial.on('data', gotData); // Event: wenn neue Daten kommen
serial.clear(); // Empfangspuffer leeren
Als nächstes erstellen wir einen Button zum Hochladen einer Bilddatei.
Wir positionieren und stylen ihn mit der CSS-Klasse custom-button
.
// Datei-Upload-Button erstellen
fileInput = createFileInput(handleFile);
fileInput.position(20, 20);
fileInput.class('custom-button');
Wir erstellen einen Button zum An-/Ausschalten der Partikelbewegung und verbinden ihn mit der Funktion toggleMotion
(an/aus).
// Eigenbewegung-Button
motionButton = createButton('Eigenbewegung: An');
motionButton.position(20, 260);
motionButton.mousePressed(toggleMotion);
motionButton.class('motion-button');
Wir erstellen drei Slider mit farblich passenden Beschriftungen für Größe, Bewegung und Verdrängung der Partikel und platzieren sie.
// Drei Slider mit Beschriftungen
let sizeLabel = createDiv("Partikelgröße").position(20, 90).addClass('label-turquoise');
sizeSlider = createSlider(0.5, 10, 4, 0.1).position(20, 110);
let driftLabel = createDiv("Partikelweg").position(20, 150).addClass('label-white');
driftSlider = createSlider(0, 1, 0.5, 0.01).position(20, 170);
let pushLabel = createDiv("Verdrängungskraft").position(20, 210).addClass('label-orange');
pushSlider = createSlider(0.1, 20, 6, 0.1).position(20, 230);
}
Funktion gotData()
empfängt Daten von Arduino und aktualisiert die Werte (Größe, Bewegung, Verdrängung, Bewegung an/aus) und passt die Slider dementsprechend an.
function gotData() {
let currentString = serial.readLine().trim(); // Neue Zeile lesen und Leerzeichen entfernen
if (!currentString) return; // Falls nichts ankommt: abbrechen
latestData = currentString;
let values = split(latestData, ','); // Trenne bei Kommas
if (values.length >= 4) {
sizeVal = map(int(values[0]), 0, 1023, 0.5, 10);
driftVal = map(int(values[1]), 0, 1023, 0, 1);
pushVal = map(int(values[2]), 0, 1023, 0.1, 20);
motionEnabled = int(values[3]) === 1;
// Setze Slider auf neue Werte
sizeSlider.value(sizeVal);
driftSlider.value(driftVal);
pushSlider.value(pushVal);
}
}
Mit loadImage
laden wir ein Bild und erzeugen daraus sichtbare Partikel basierend auf den Pixel-Farben.
Wir speichern die Partikel in einem Array.
function handleFile(file) {
if (file.type === 'image') {
img = loadImage(file.data, () => {
img.resize(400, 0); // Maximalbreite
particles = []; // Partikel-Array leeren
for (let x = 0; x < img.width; x += 4) {
for (let y = 0; y < img.height; y += 4) {
let c = img.get(x, y); // Farbe des Pixels
if (alpha(c) > 0) { // Nur sichtbare Pixel übernehmen
particles.push(new Particle(x, y, c));
}
}
}
imgReady = true; // Bild geladen
});
}
}
Jetzt definieren wir eine eigene Klasse, die regelt, wie sich ein Partikel bewegt und angezeigt wird.
Jeder Partikel bewegt sich leicht zufällig, kehrt zur Ursprungsposition zurück und wird bei Mausnähe abgestoßen.
Die Darstellung ist per Reset zurücksetzbar.
class Particle {
constructor(x, y, c) {
this.home = createVector(x, y); // Startposition merken
this.pos = createVector(x, y); // Aktuelle Position
this.vel = p5.Vector.random2D().mult(random(0.5)); // Zufällige Anfangsbewegung
this.color = c;
}
update() {
let mouse = createVector(
mouseX - width / 2 + img.width / 2,
mouseY - height / 2 + img.height / 2
);
let dir = p5.Vector.sub(this.pos, mouse);
let d = dir.mag(); // Abstand zur Maus
if (d < 60) {
dir.setMag(pushVal); // Wegdrücken
this.vel.add(dir);
}
let toHome = p5.Vector.sub(this.home, this.pos).mult(0.05 * (1 - driftVal));
this.vel.add(toHome);
if (motionEnabled) {
this.vel.add(p5.Vector.random2D().mult(0.1)); // Leichte Eigenbewegung
}
this.vel.limit(5);
this.pos.add(this.vel);
this.vel.mult(0.9); // Bewegung dämpfen
}
display() {
fill(this.color);
let stretch = map(this.vel.mag(), 0, 5, 1, 2.5);
ellipse(this.pos.x, this.pos.y, sizeVal * stretch, sizeVal);
}
reset() {
this.pos = this.home.copy();
this.vel = createVector(0, 0);
}
}
draw()
-Loop zeigt alle Partikel zentriert an und aktualisiert ihre Bewegung bei jedem Frame..value
fragt aktuelle Werte der Slider ab.
function draw() {
background(0);
if (!imgReady) {
text('Bitte eine Datei auswählen :)', 20, 330);
return;
}
sizeVal = sizeSlider.value();
driftVal = driftSlider.value();
pushVal = pushSlider.value();
translate(width / 2 - img.width / 2, height / 2 - img.height / 2);
for (let p of particles) {
p.update();
p.display();
}
}
Interaktive Funktionen:
mousePressed
setzt alle Partikel zurück, wenn die Maus gedrückt wird.windowResized
passt die Canvasgröße an, wenn das Fenster geändert wird.toggleMotion
schaltet die Eigenbewegung der Partikel an oder aus und aktualisiert den Button-Text.
function mousePressed() {
for (let p of particles) {
p.reset();
}
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
function toggleMotion() {
motionEnabled = !motionEnabled;
motionButton.html(motionEnabled ? 'Eigenbewegung: An' : 'Eigenbewegung: Aus');
}
Jetzt ist die Webversion fertig!
Um die Anwendung jetzt auch in die physische Welt zu übertragen, benötigen wir einen Arduino Uno Baukasten und weitere Codes.
Arduino Bauteile
ARDUINO BAUTEILE | CODE DATEIEN |
Arduino Uno | Arduino IDE |
Steckbrett | p5 Serial Control |
Potentiometer 3x | p5.js Web Editor |
Taster / Button | HTML |
Kabel 13x | CSS |
Arduino Aufbau
Der Arduino wird nun Schritt für Schritt aufgebaut und verbunden.
Zuerst wird das Steckbrett mit dem Arduino verbunden. Das Rote Kabel führt von 5V zu +, das schwarze von GND zu - .
Die Pottis werden mit dem einen Pin an A0, A1, und A2 im Arduino gesteckt. Die beiden anderen Pins werden mit einmal + und einmal - auf dem Steckbrett verbunden.
Der Button wird an einem Pin mit GND und am anderen mit Digital 2 verbunden.
Verbindung von Arduino und p5.js mithilfe von Arduino IDE | Serial Control Port | HTML | CSS
Um die Anwendungen zu koppeln brauchen wir verschiedene Codedateien.
Um Arduino und p5.js zu verbinden, braucht man mehrere Bausteine, weil Arduino die Hardware steuert und p5.js im Browser läuft.
Das HTML sorgt dafür, dass die Seite überhaupt existiert, und ein Serial Control Port überträgt die Daten zwischen beiden, damit sie sich koppeln.
Arduino IDE
Zuallererst werden mit const int
die Konstanten definiert.
pot1Pin
, pot2Pin
und pot3Pin
stehen für die Pins A0, A1 und A2, an die Potentiometer angeschlossen sind.
buttonPin
steht für den digitalen Pin 2, an dem ein Button angeschlossen ist.
const int pot1Pin = A0;
const int pot2Pin = A1;
const int pot3Pin = A2;
const int buttonPin = 2;
Als nächstes wird mit lastButtonState
und motionState
der Zustand des Buttons gespeichert, das ist wichtig, damit die Eigenbewegung dauerhaft aktiviert bzw. deaktiviert ist, und nicht nur während der Button gedrückt wird.
Serial.begin(9600)
startet die serielle Kommunikation mit dem PC.
Durch pinMode(buttonPin, INPUT_PULLUP)
schaltet der Arduino intern einen Pullup-Widerstand ein, der den Pin ohne Knopfdruck auf HIGH (5V) hält, und wenn der Button gedrückt wird, zieht er den Pin auf LOW (0V).
int lastButtonState = LOW;
bool motionState = true;
void setup() {
Serial.begin(9600);
pinMode(buttonPin, INPUT_PULLUP);
}
Nun werden die Potis und der Button eingelesen, und eine Loop Funktion gestartet.
analogRead
liest den aktuellen Wert der Potentiometer aus (Wertebereich 0–1023).
digitalRead
liest den aktuellen Zustand des Buttons (HIGH oder LOW).
int pot1 = analogRead(pot1Pin);
int pot2 = analogRead(pot2Pin);
int pot3 = analogRead(pot3Pin);
int buttonState = digitalRead(buttonPin);
if (buttonState == LOW && lastButtonState == HIGH)
prüft, ob der Button gerade gedrückt wurde, LOW bedeutet gedrückt (wegen INPUT_PULLUP
).
motionState = !motionState
invertiert den Bewegungszustand (true
zu false
etc.)
delay(...)
entschleunigt für eine saubere Ausführung des Codes.
Mit serial.print
werden die Werte ausgegeben.
if (buttonState == LOW && lastButtonState == HIGH) {
motionState = !motionState;
delay(50);
}
lastButtonState = buttonState;
Serial.print(pot1);
Serial.print(",");
Serial.print(pot2);
Serial.print(",");
Serial.print(pot3);
Serial.print(",");
Serial.println(motionState ? 1 : 0); //Wenn motionState true ist, wird eine 1 gesendet, sonst eine 0.
delay(20);
P5 Serial Control
Da das Arduino Programm alleine, keine Verbindung zu dem Web Editor p5.js aufbauen kann, müssen wir es mit dem Programm SerialControl verbinden.
Lade das Programm bei Github herunter und wähle den richtigen Port an deinem Computer aus (in unserem Fall "COM6"). In Arduino IDE wird dir angezeigt mit welchem Port der Arduino Uno verbunden ist.
Scanne die Ports
Wähle den richtigen Port
öffne den Port
Nun aktivierst du die Häckchen bei "console enabled" und "read in ASCII", nun wird dir in der Konsole angezeigt, ob deine Potentiometer und der Taster am Arduino, Daten an deinen Computer senden.

HTML
In der HTML Datei werden die Codes der anderen Dateien eingebunden, um so die unterschiedlichen Anwendungen miteinander zu koppeln.
Bindet die Hauptbibliothek von p5.js ein
Fügt die p5.sound-Bibliothek hinzu, um Audio zu analysieren oder wiederzugeben (optional, evt. für weiterarbeit)
Verlinkt das CSS Stylesheet welches dafür verantwortlich ist, alles angenehm zu gestalten.
Bindet die p5.serialport-Bibliothek ein, um serielle Kommunikation mit dem Arduino zu ermöglichen.
Da der p5.js Code auf der Seite zu sehen sein soll und im Web Editor bearbeitet wird, wird das p5.js Script im Body der HTML Datei verlinkt.
<!DOCTYPE html>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/npm/p5@1.11.5/lib/p5.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.3/addons/p5.sound.min.js"></script>
<link rel="stylesheet" type="text/css" href="style.css">
<meta charset="utf-8" />
<script src="https://cdn.jsdelivr.net/npm/p5.serialserver@latest/lib/p5.serialport.js"></script>
</head>
<body>
<script src="sketch.js"></script>
</body>
</html>
CSS
In der CSS Stylesheet Datei befinden sich die Variablen für die Gestaltung der digitalen Einzelelemente.
Dabei geht es vor Allem um die Buttons, Slider und Label.
/* Canvas */
canvas {
display: block;
}
/* Allgemeine Einstellungen */
html, body {
margin: 0;
padding: 0;
font-family: 'Arial', sans-serif;
font-size: 16px;
font-weight: 400;
}
Um die Buttons etwas interessanter und angenehmer zu gestalten haben wir deren Größe/ Farbe und Hoveranimation angepasst.
/* Buttons */
.custom-button, .motion-button {
padding: 8px 20px;
font-size: 16px;
font-family: Arial, sans-serif;
border: none;
border-radius: 10px;
cursor: pointer;
}
.custom-button {
background-color: white;
color: black;
}
.custom-button:hover {
background-color: lightgray;
}
.custom-button span {
font-size: 16px;
}
.motion-button {
background-color: white;
color: black;
border-radius: 8px;
transition: background-color 0.3s ease, color 0.3s ease;
}
.motion-button:hover {
background-color: lightgray;
}
.motion-button:active {
background-color: black;
color: white;
}
.motion-button.active {
background-color: black;
color: white;
border: 2px solid white;
}
Den Slidern haben wir farbige Labels gegeben (passend zu den Potentiometern am Arduino) und deren Länge responsiv an die Fenstergröße angepasst.
/* Slider */
input[type=range] {
margin-top: 5px;
-webkit-appearance: none;
appearance: none;
width: 15%;
height: 8px;
background-color: lightgray;
border-radius: 5px;
}
/* Labels */
.label-turquoise {
color: turquoise;
font-family: Arial, sans-serif;
}
.label-white {
color: white;
font-family: Arial, sans-serif;
}
.label-orange {
color: orange;
font-family: Arial, sans-serif;
}
Das war´s auch schon! Danke fürs Vorbeischauen und viel Spaß :)
Hier geht´s zum Code im Web Editor.