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.

  1. Scanne die Ports

  2. Wähle den richtigen Port

  3. ö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.

  1. Bindet die Hauptbibliothek von p5.js ein

  2. Fügt die p5.sound-Bibliothek hinzu, um Audio zu analysieren oder wiederzugeben (optional, evt. für weiterarbeit)

  3. Verlinkt das CSS Stylesheet welches dafür verantwortlich ist, alles angenehm zu gestalten.

  4. Bindet die p5.serialport-Bibliothek ein, um serielle Kommunikation mit dem Arduino zu ermöglichen.

  5. 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.

Schlagworte


© 0t1

Cookies

0t1 mag keine Kekse (und kein Tracking). Wir verwenden lediglich notwendige Cookies für essentielle Funktionen.

Wir verwenden Schriftarten von Adobe Fonts. Dafür stellt dein Browser eine Verbindung zu den Servern von Adobe in den USA her. Wenn du unsere Seite nutzen möchtest, musst du dich damit einverstanden erklären.

Weitere Informationen in unserer Datenschutzerklärung.