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:
Hardware: Dein Arduino benötigt einen Temperatursensor (z. B. TMP36 oder LM35)
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!

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(Temperatur
zeigt die geglättete Temperatur in lesbarer Form als Text oben links auf dem Canvas.

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