Von Temperatur zu Bewegung: Interaktives Partikelsystem mit Arduino & p5.js

In diesem Tutorial wird gezeigt, wie ein Arduino-Temperatursensor mit einem interaktiven Partikelsystem in p5.js verbunden werden kann. Die Sensordaten werden dabei in eine visuelle, dynamische Darstellung übersetzt. Durch Temperaturschwankungen ändern die Partikel ihre Geschwindigkeit, Größe und Farbe.

1. Voreinstellungen

Bevor wir mit dem Code beginnen, solltest du sicherstellen, dass folgende Dinge vorhanden sind:

  1. Hardware: Dein Arduino benötigt einen Temperatursensor (z. B. TMP36 oder LM35)

  2. Software: Du brauchst die Arduino IDE, um deinen Arduino mit deinem Computer zu verbinden, und einen Webbrowser, um die Visualisierung auszuführen. (z. B. Chrome)

2. Aufbau

Benötigte Teile:

Arduino Uno

TMP36 Temperatursensor

Breadboard

2 Jumperkabel

3 Kabel (1 längeres, 2 kurze)

USB-Kabel

Schaltplan:

Stecke den Sensor so ins Breadboard, dass die flache Seite zu dir zeigt. Verbinde das linke Bein des Sensors mit dem 5V-Anschluss des Arduino (rotes Kabel), das mittlere Bein mit A0 (grünes Kabel) und das rechte Bein mit GND (schwarzes Kabel). Zusätzlich verbindest du die Plus-Schiene des Breadboards mit 5V und die Minus-Schiene mit GND, jeweils mit kurzen Kabeln. So wird der Sensor richtig mit Strom versorgt und die Temperatur kann sauber gemessen werden.

Wichtig: Achte auf die Ausrichtung des TMP36!

Schaltplan

3. Arduino Code

Zuerst programmierst du den Arduino mit der Arduino IDE, damit er die Temperatur misst und über die serielle Schnittstelle an den Computer sendet.

// Temperaturmessung mit TMP36 (oder LM35) Sensor
int sensorPin = A0;  
float temperature = 0;

void setup() {
  Serial.begin(9600);  
}

void loop() {
  // Sensorwert einlesen
  int sensorValue = analogRead(A0);
float voltage = (sensorValue/1024.0) * 5.0; //5 Volt Referenzspannung
float temperatureC = (voltage - 0.5) * 100.0;
  
  // Temperatur berechnen in Grad Celsius (für TMP36)
  temperature = (sensorValue * 5.0 / 1023.0) * 100.0;

  // Temperatur über die serielle Schnittstelle senden
  Serial.println(temperatureC);
  
  delay(200);  
}

Serial.begin startet die serielle Kommunikation.

analogRead liest den Sensorwert ein.

Serial.println(temperatureC); schickt den aktuellen Temperaturwert vom Arduino über die serielle Schnittstelle als komplette Zeile mit Zeilenumbruch an den Computer.

Der delay sorgt für eine Pause, bevor der nächste Wert gesendet wird damit die Partikel sich besser an den aktuellen Wert anpassen können.

4. Initialisierung

Jetzt verbindest du den Arduino mit p5.js:

Achte darauf, dass der portName in der sketch.js der korrekte serielle Port für deinen Arduino ist. Auf Windows könnte das z. B. COM3 sein, auf macOS /dev/tty.usbmodem101 oder ähnlich.

Lade den Arduino-Code auf dein Arduino-Board hoch. (Wichtig: Es darf nur ein Serial Port offen sein. Achte darauf den Arduino Port zu schließen, bevor der p5.serialport geöffnet wird)

Öffne den p5.serialport um hier die Temperatur auszulesen. Dies ist notwendig um diese in dein p5 Projekt einbinden zu können. (Wichitg: Achte darauf die Temperatur in ASCII ausgeben zu lassen)

Nun kannst du den script src Link zur externen Bibliothek aus dem p5.serialport kopieren und in den Header deiner index.html Datei einfügen um die serielle Kommunikation zwischen Arduino und p5.js zu ermöglichen.

Nun kannst du deinen Sketch in p5.js starten.

5. Creative Coding

Jetzt richtest du das p5.js-Projekt ein, das die Temperatur liest und die Partikel entsprechend anzeigt.

1. HTML-Datei

Erstelle eine HTML-Datei (index.html) und füge den folgenden Code ein:

<!DOCTYPE html>
<html lang="de">
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.11.1/p5.js"></script>
    <link href="https://fonts.googleapis.com/css2?family=Roboto&display=swap" rel="stylesheet">
    <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>
    <meta charset="utf-8" />
  </head>
  <body>
    <main></main>
    <script src="sketch.js"></script>
  </body>
</html>

Die index.html verbindet alle wichtigen Bausteine des Projekts: Sie lädt externe Bibliotheken (p5.js, p5.serialport), bindet die gewählte Schriftart Roboto ein, verlinkt das eigene CSS für das Layout und lädt schließlich das Hauptprogramm (sketch.js), damit die Animation im Browser korrekt startet.

Die externen Bibliotheken sind deshalb wichtig weil sie dir ermöglichen, die serielle Kommunikation vom Arduino zu empfangen und die Visualisierung zu steuern.

2. Sketch-Datei

Erstelle nun eine sketch.js-Datei, die den Code für die Visualisierung enthält:

2.1 Grundlegende Variablen:

let serial;
let portName = '/dev/tty.usbmodem101';
let temperature = 18;
let smoothedTemp = 18;
let particles = [];
let gridSpacing = 40;

Hier bereitest du alles Wichtige vor damit das Programm später einfach und ordentlich auf diese Werte zugreifen kann:

serial für die serielle Verbindung mit dem Temperatursensor.

portName, damit das Programm weiß, welchen Port es öffnen soll.

temperature und smoothedTemp speichern die aktuelle Temperatur, wobei smoothedTemp das ganze glättet für eine bessere visuelle Ausgabe.

particles ist die Liste der kleinen Kreise (Partikel), die später gezeichnet werden.

gridSpacing brauchst du, damit bei Kälte die Partikel auf einem Gitter sitzen.

2.2 setup() – Alles einmal starten

function setup() {
  createCanvas(600, 600);
  colorMode(HSB, 360, 100, 100, 100);
  textFont("Roboto");
  textSize(20);
  noStroke();
  
  serial = new p5.SerialPort();
  serial.open(portName);
  serial.on('data', gotData);

  for (let i = 0; i < 300; i++) {
    particles.push(new Particle(random(width), random(height)));
  }
}

createCanvas und colorMode richten die visuelle Umgebung ein.

serial.open verbindet das Programm mit dem Sensor.

Wir starten mit 300 Partikeln die zufällig auf dem Canvas erzeugt werden. Ihr Verhalten wird später angepasst.

2.3 Temperatur vom Sensor lesen

function gotData() {
  let currentData = serial.readLine().trim();
  if (currentData && !isNaN(currentData)) {
    temperature = float(currentData);
  }
}

Bei neuen seriellen Daten wird der Temperaturwert mit der gotData Funktion geupdated. So kann die Animation in Echtzeit auf die aktuelle Temperatur reagieren.

2.4 Alles wieder und wieder zeichnen

function draw() {
  smoothedTemp = lerp(smoothedTemp, temperature, 0.05);
  background(270, 20, 10, 10);

  for (let p of particles) {
    p.update(smoothedTemp);
  }

  particles.sort((a, b) => b.z - a.z);
  for (let p of particles) {
    p.display();
  }

  fill(0, 0, 100);
  text(`Temperatur: ${nf(smoothedTemp, 2, 2)} °C`, 20, 30);
}

Die draw() Funktion sorgt kontinuierlich dafür, dass Bewegung, Animation und Temperaturdarstellung dynamisch bleiben.

smoothedTemp = lerp glättet (interpoliert) die Temperaturwerte für weichere Übergänge.

background(270, 20, 10, 10); zeichnet einen halbtransparenten Hintergrund (HSB-Farbmodus) das erzeugt einen Schweif-Effekt.

p.update ruft für jeden Partikel die update()-Methode auf damit sich die Partikelbewegung und Eigenschaften je nach Temperatur anpassen.

text(Temperaturzeigt die geglättete Temperatur in lesbarer Form als Text oben links auf dem Canvas.

Visuelle Darstellung der Temperaturanzeige

2.5 Die Particle-Klasse und Konstruktor

Die Particle-Klasse beschreibt, was ein einzelner Partikel ist und wie es sich verhält.

class Particle {
constructor(x, y) {
  this.pos = createVector(x, y); //Position auf dem Canvas
  this.vel = p5.Vector.random2D().mult(0.03); //zufällige Richtung, verlangsamt
  this.acc = createVector(); 
  this.hue = 270; //Farbe im HSB-Farbraum
  this.size = 5; // aktuelle Größe, startet bei 5
  this.targetSize = 5;
  this.noiseOffset = random(1000); //Zufallswert für individuelle Bewegung
  this.z = random(100); //Tiefe (helligkeitsunterschiede der Partikel)
}

Die Werte wurden bewusst so gewählt, damit jeder Partikel sich wie ein Molekül verhält. Du kannst sie natürlich nach belieben anpassen.

Visuelle Darstelung der Partikel, Temperatur unabhängig

2.6 Update-Methode

update(temp) { ... }

Diese Funktion wird für jedes Partikel in jedem Frame aufgerufen damit die Partikel aktuell auf die Temperatur reagieren. (Zum Beispiel für die Aktualisierung der entsprechenden Farben)

2.7 Geschwindigkeit, Größe und Farbe anpassen

let baseSpeed = map(temp, 15, 22, 0.01, 3.0);
baseSpeed = constrain(baseSpeed, 0.01, 3.0);

Wenn es wärmer wird, werden die Partikel schneller, bei niedriger Temperatur sind sie fast still.

Mit map() übersetzen wir Temperatur in Geschwindigkeit.

constrain() sorgt dafür, dass die Werte nicht übersteuern.

Visuelle Darstellung der Partikelgeschwindigkeit mit Abhängigkeit der Temperatur
this.targetSize = map(temp, 15, 22, 5, 70);
this.size = lerp(this.size, this.targetSize, 0.1);

Bei höherer Temperatur wachsen die Partikel.

lerp() (linear interpolation) sorgt dafür, dass die Änderung sanft passiert und nicht springt.

if (deviation < 0) {
  this.hue = map(deviation, -5, 0, 200, 270);
} else {
  this.hue = map(deviation, 0, 5, 270, 360);
}

Die Variable deviation misst, wie stark die aktuelle Temperatur von einem festgelegten Referenzwert (in diesem Fall 17 °C) abweicht.

hue ist Teil des HSB-Farbmodus (Hue, Saturation, Brightness), damit steuern wir die Farbunterschiede.

In der update()-Methode (Die sich in der Particle Klasse befindet) wird die Farbe dann dynamisch je nach Temperatur angepasst:

Bei Temperaturen unter 17 °C sinkt hue Richtung 200 das ergibt dann kühlere Farben (Blau-Grün).

Bei Temperaturen über 17 °C steigt hue steigt Richtung 360 was für wärmere Farben sorgt (Rot-Orange).

Visuelle Darstellung der Partikel, Größe und Farbe abhängig von Temperatur

2.8 Bewegung mit Perlin-Noise erzeugen:

let noiseScale = 0.005;
let angle = noise(this.pos.x * noiseScale, this.pos.y * noiseScale, this.noiseOffset) * TWO_PI * 4;
let dir = p5.Vector.fromAngle(angle);
this.acc = dir;
this.vel.lerp(this.acc, 0.2);
this.vel.setMag(baseSpeed);

Um eine natürliche, sanfte Bewegung zu erzeugen nutzen wir Perlin-Noise. So kommt kein hektisches Zucken zustande.

lerp() mischt die alte Bewegung leicht in die neue Richtung.

Danach setzt du die Geschwindigkeit passend zur Temperatur.

2.9 Verfeinerung

Gitter bei Kälte, Abprallen der Partikel von den Grenzen, Pulsieren bei Wärme

if (temp <= 17) {
  let gridX = round(this.pos.x / gridSpacing) * gridSpacing;
  let gridY = round(this.pos.y / gridSpacing) * gridSpacing;
  let gridTarget = createVector(gridX, gridY);

  let attractionStrength = map(temp, 15, 17, 0.1, 0);
  attractionStrength = constrain(attractionStrength, 0, 0.1);
  let attraction = p5.Vector.sub(gridTarget, this.pos).mult(attractionStrength);
  this.vel.add(attraction);

  let zMovement = map(temp, 15, 17, 0.01, 5);
  this.z = lerp(this.z, this.z + zMovement, 0.1);
}

    this.pos.add(this.vel);
    this.noiseOffset += 0.01;

Bei Kälte bewegen sich die Partikel auf definierte Gitterpunkte zu, dieses Verhalten gleicht dann dem Verhalten von Wassermolekülen beim gefrieren.

Je kälter, desto stärker die Anziehung. Die Partikel behalten trotzdem eine leichte Bewegung, um nicht komplett statisch zu wirken.

if (temp <= 17) überprüft, ob die Temperatur 17°C oder weniger beträgt. Nur dann wird der nachfolgende Code ausgeführt.

let gridX und let gridY berechnen die nächsten Gitterpunkte auf der X- und Y-Achse, der dem Partikel am nächsten ist.

let gridTarget erstellt einen Vektor für den Zielgitterpunkt und let attractionStrength berechnet die Stärke der Anziehung.

attractionStrength = constrain stellt sicher, dass die Anziehungskraft nicht über 0.1 hinausgeht.

p5.Vector.sub(gridTargetthis.pos).mult(attractionStrength); berechnet die Anziehungskraft (Vektor) zwischen dem Partikel und dem Zielgitterpunkt und multipliziert sie mit der berechneten Stärke.

this.vel.add(attraction);fügt dann die berechnete Anziehungskraft zur Geschwindigkeit des Partikels hinzu.

Visuelle Darstellung des Gitters
let margin = 20;
    if (this.pos.x < margin || this.pos.x > width - margin) {
      this.vel.x *= -1;
      this.pos.x = constrain(this.pos.x, margin, width - margin);
    }
    if (this.pos.y < margin || this.pos.y > height - margin) {
      this.vel.y *= -1;
      this.pos.y = constrain(this.pos.y, margin, height - margin);
    }

Wenn ein Partikel den Rand berührt, wird die Bewegung reflektiert.

if (temp > 18) {
  let pulseStrength = map(temp, 18, 22, 0.6, 4);
  this.size += sin(frameCount * 0.3 + this.noiseOffset) * pulseStrength;
}

Wenn der Temperatursensor Temperaturen über 18 Grad empfängt, fangen die Partikel an zu pulsieren. Je höher die Temperatur desto stärker das Pulsieren. Die Inspiration hierfür ist kochendes, blubberndes Wasser.

Visuelle Darstellung der pulsierenden Partikel

2.10 Display-Methode

display() {
  let depthEffect = map(this.z, 0, 100, 0.1, 1.0);
  fill(this.hue, 90, 100, depthEffect * 60);
  ellipse(this.pos.x, this.pos.y, this.size, this.size);
}

Die Helligkeit des Partikels hängt von seiner Tiefe ab: weiter hinten = dunkler, weiter vorne = heller.

Dann wird ein einfacher Kreis (ellipse) an der Position des Partikels gezeichnet damit das Bild dreidimensionaler und lebendiger wirkt.

Visuelle Darstellung der Tiefe und Schweife

Alles in einem:

Code der sketch.js-Datei für die Visualisierung:

let serial;
let portName = '/dev/tty.usbmodem101';
let temperature = 18;
let smoothedTemp = 18;
let particles = [];
let gridSpacing = 40;

function setup() {
  createCanvas(600, 600);
  colorMode(HSB, 360, 100, 100, 100);
  textFont("Roboto");
  textSize(20); // Etwas größere Schrift
  noStroke();

  serial = new p5.SerialPort();
  serial.open(portName);
  serial.on('data', gotData);

  for (let i = 0; i < 300; i++) {
    particles.push(new Particle(random(width), random(height)));
  }
}

function gotData() {
  let currentData = serial.readLine().trim();
  if (currentData && !isNaN(currentData)) {
    temperature = float(currentData);
  }
}

function draw() {
  smoothedTemp = lerp(smoothedTemp, temperature, 0.05);
  background(270, 20, 10, 10);

  for (let p of particles) {
    p.update(smoothedTemp);
  }

  particles.sort((a, b) => b.z - a.z);
  for (let p of particles) {
    p.display();
  }

  fill(0, 0, 100);
  text(`Temperatur: ${nf(smoothedTemp, 2, 2)} °C`, 20, 30);
}

class Particle {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.vel = p5.Vector.random2D().mult(0.03);
    this.acc = createVector();
    this.hue = 270;
    this.size = 5;
    this.targetSize = 5;
    this.noiseOffset = random(1000);
    this.z = random(100);
  }

  update(temp) {
    let deviation = temp - 17;

    // Geschwindigkeit
    let baseSpeed = map(temp, 15, 22, 0.01, 3.0);
    baseSpeed = constrain(baseSpeed, 0.01, 3.0);

    // Größe
    this.targetSize = map(temp, 15, 22, 5, 70);
    this.size = lerp(this.size, this.targetSize, 0.1);

    // Farbe
    if (deviation < 0) {
      this.hue = map(deviation, -5, 0, 200, 270);
    } else {
      this.hue = map(deviation, 0, 5, 270, 360);
    }

    let noiseScale = 0.005;
    let angle = noise(this.pos.x * noiseScale, this.pos.y * noiseScale, this.noiseOffset) * TWO_PI * 4;
    let dir = p5.Vector.fromAngle(angle);
    this.acc = dir;
    this.vel.lerp(this.acc, 0.2);
    this.vel.setMag(baseSpeed);

    // Gitter bei Kälte
    if (temp <= 17) {
      let gridX = round(this.pos.x / gridSpacing) * gridSpacing;
      let gridY = round(this.pos.y / gridSpacing) * gridSpacing;
      let gridTarget = createVector(gridX, gridY);

      let attractionStrength = map(temp, 15, 17, 0.1, 0);
      attractionStrength = constrain(attractionStrength, 0, 0.1);
      let attraction = p5.Vector.sub(gridTarget, this.pos).mult(attractionStrength);
      this.vel.add(attraction);

      let zMovement = map(temp, 15, 17, 0.01, 5);
      this.z = lerp(this.z, this.z + zMovement, 0.1);
    }

    this.pos.add(this.vel);
    this.noiseOffset += 0.01;

    // Grenzen
    let margin = 20;
    if (this.pos.x < margin || this.pos.x > width - margin) {
      this.vel.x *= -1;
      this.pos.x = constrain(this.pos.x, margin, width - margin);
    }
    if (this.pos.y < margin || this.pos.y > height - margin) {
      this.vel.y *= -1;
      this.pos.y = constrain(this.pos.y, margin, height - margin);
    }

    // Pulsieren bei Wärme
    if (temp > 18) {
      let pulseStrength = map(temp, 18, 22, 0.6, 4);
      this.size += sin(frameCount * 0.3 + this.noiseOffset) * pulseStrength;
    }
  }

  display() {
    let depthEffect = map(this.z, 0, 100, 0.1, 1.0);
    fill(this.hue, 90, 100, depthEffect * 60);
    ellipse(this.pos.x, this.pos.y, this.size, this.size);
  }
}

Jeder Partikel reagiert nun eigenständig auf die Temperatur: Er wird schneller, größer, farbiger, geordneter oder pulsierender mit sanften Übergängen, Perlin-Noise und Tiefe.

3. CSS-Datei

Erstelle nun eine einfache style.css-Datei, um das Layout zu verbessern:

html, body {
  margin: 0;
  padding: 0;
  font-family: 'Roboto', sans-serif;
}

canvas {
  display: block;
}

Die CSS-Datei entfernt Browser-Standard-Abstände, setzt die gewählte Schriftart und sorgt dafür, dass das Canvas sauber und vollflächig dargestellt wird.

6. Visualisierung testen

Wenn du alles richtig eingerichtet hast, solltest du nun eine Canvas sehen, auf der Partikel basierend auf der Temperaturwerte vom Arduino angezeigt werden. Die Partikel verändern ihre Größe, Farbe und Bewegung je nach gemessener Temperatur.

Finale Grafische Visualisierung

7. Mögliche Erweiterungen

Durch die sketch.js-Datei hast du jetzt eine Grundlage, bei der du viele Eigenschaften des visuellen Systems einfach anpassen oder erweitern kannst. (Du kannst die Farbe, Größe und Geschwindigkeit der Partikel jederzeit nach belieben verändern)

Mögliche Erweiterungen für das gesamte Projekt wären, z.B. ein Potentiometer als neuen Sensor anschließen, die Farben dynamisch an die Tageszeit anpassen, die Partikel auf Mausbewegungen reagieren zu lassen oder Hintergrundeffekte und Sounds zu ergänzen.

8. Link und Quellenverzeichnis

Link zum P5.js Sketch:

https://editor.p5js.org/nikayo/sketches/4N1DRU0rK

Quellen:

https://www.tutorialspoint.com/arduino/arduino_temperature_sensor.htm

https://p5js.org/examples/repetition-noise/

https://archive.p5js.org/examples/simulate-particles.html


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