Partikelsysteme mit Flowfields manipulieren in p5.js

In diesem Tutorial zeigen wir dir, wie du ein dynamisches Partikelsystem in p5.js erstellen und über ein FlowField beeinflussen kannst. Mit interaktiven Eingaben steuerst du das Verhalten der Partikel live und hast dadurch eine ideale Möglichkeit ein Tieferes Verständnis darüber zu gewinnen – schau doch einfach mal rein!

Übersicht: Was macht der Code?

Der Sketch in p5.js erzeugt eine dynamische Partikelwelt, in der sich Tausende von Partikeln entlang eines sich stetig verändernden Vektorfeldes bewegen.

Die Bewegung wird durch Perlin Noise gesteuert, was sehr organische, fließende Muster entstehen lässt.

Über seriell angeschlossene Hardware (Buttons und Potentiometer) kann der Nutzer live verschiedene Eigenschaften steuern: von der Geschwindigkeit der Partikel bis zur Größe des Rasters, auf dem sie sich bewegen.

Wichtige Bestandteile

1. Flowfield

Ein Flowfield (auf Deutsch manchmal Strömungsfeld genannt) ist eine Art “unsichtbare Landkarte” von Richtungen und Bewegungen in einem Raum.

Genauer gesagt:

  • An jedem Punkt im Raum gibt es eine Richtung (ein Vektor), die angibt, wohin sich etwas an diesem Punkt bewegen würde.

  • Das kann z. B. die Richtung des Windes sein, die Bewegung von Wasser, oder – in der Computergrafik – die Bewegungsrichtung von Partikeln.

In unserem Fall wrid das Flowfield als Gitter von Linien dargestellt:

  • Jede Linie zeigt die Bewegungsrichtung an seinem Punkt.

  • Partikel können dann dieses Flowfield „lesen“ und sich daran orientieren.

2. Perlin Noise

Perlin Noise ist eine besondere Art von “Zufall”, der glatt und natürlich wirkt.Statt komplett chaotisch (wie normaler Zufall), ändert sich Perlin Noise sanft über Raum oder Zeit. Man kann sich das vorstellen wie weiche Hügel in einer Landschaft oder sanfte Wellen im Wasser.

Technisch:

  • Normale Zufallszahlen springen wild hin und her: 0.1, 0.9, 0.2, 0.8, usw.

  • Perlin Noise erzeugt Zahlen, die fließend ineinander übergehen: 0.1 → 0.15 → 0.18 → 0.21, usw.

In Unserem Fall Steuern wir die Winkel der Vektoren in unserem Flowfield. Der Code dazu sieht so aus:

let angle = noise(xoff, yoff, zoff) * TWO_PI * 4;

Die 3 Werte xoff yoff und zoff steuern die Änderung der Winkel

TWO_PI sorgt dafür das sich die Winkel um 360 Grad ändern können

*4 ist ein Multiplikator um die Änderungen zu verstärken

zoff steuert die Geschwindigkeit der Änderung des Winkels.

xoff steuert die Differenz der Vektorwinkel in der Waagrechten zueinander

yoff steuert die Differenz der Vektorwinkel in der Senkrechten zueinander

Die Kombination dieser Werte erzeugt organisch wirkende Muster, die je nach Intensität der Manipulation von ruhig und geordnet bis hin zu lebendig und chaotisch wirken können.

3. Partikel

Ein Partikelsystem ist ein Trick, mit dem viele kleine Teilchen (Partikel) zusammen benutzt werden, um etwas Großes oder Lebendiges darzustellen.

Einfach gesagt:

  • Ein Partikel ist ein winziges Objekt: ein Punkt, ein kleiner Kreis, manchmal nur eine Idee von etwas.

  • Viele Partikel zusammen verhalten sich wie Rauch, Feuer, Wasser, Funken oder Schwärme.

  • Jeder Partikel hat oft Eigenschaften wie Position, Geschwindigkeit, Lebensdauer, manchmal auch Größe oder Farbe.

In unserem Fall kann der User bis zu 10 000 Partikel erzeugen

Das Zusammenspiel

Die Partikel bewegen sich zunächst mit einer konstanten Geschwindigkeit durch das Flowfield, also dem oben beschriebenen Feld aus Vektoren. Wenn die Partikel auf einen dieser Vektoren treffen, werden sie in die Richtung des Vektors beschleunigt. Dadurch “folgen” sie der Richtung des Vektors und ändern ihren Kurs. Das führt zu einer Bewegung, die wie natürliche Strömungen aussieht, zum Beispiel wie Wasser oder Luft. Je nachdem, wie das Flowfield verändert wird (Rastergröße / Perlinnoisewertveränderung) und wie schnell die Partikel maximal werden dürfen, sieht die Bewegung entweder ruhig oder chaotisch aus und es kann zu einer homogenen oder auch verdichteten Verteilung kommen.

Zudem können über den Code die Partikel auch einen schweif hinter sich herziehen was die Bewegung umso mehr visualisert.

    Wie der Code diese Dinge umsetzt Stück für Stück erklärt

    1. Vorbereitung: Variablen für Serielle Kommunikation und Systemzustände

    let serial;              // Serialport-Objekt
    let B1 = 0;              // linker Button
    let B2 = 0;              // rechter Button
    let lastB1 = 0;          //Flankenerkennung
    let lastB2 = 0;          //Flankenerkennung
    let Poti = 0;            // Potentiometer
    let lastPotiVal = 0;     //Potiprüfvariable
    let bgTrans=50;
    let maxspeed= 0.5;
    
    let zspeed=0.00005;            // Zgeschwindigkeitsfaktor perlin
    let yrand=0.005;             //Perlin Y randomisator
    let xrand=0.005;             //Perlin X ranomisator 
    let vectrans=0;            // Vectortransparenzwert
    let sele=0;              //selektorvariable
    
    
    
    let inc = 0.01;//variable für X Y Z der Vektoren
    let scl =60; //Skalierung des Vektorenraster
    let cols, rows; //Estellung Variablen für Raster

    Ganz am Anfang werden zahlreiche Variablen deklariert:

    • serial ist ein Objekt, das für die serielle Verbindung zuständig ist.

    • B1 und B2 sind zwei digitale Eingänge (Buttons), die gedrückt oder nicht gedrückt sein können (1 oder 0).

    • lastB1 und lastB2 speichern den vorherigen Status der Buttons, um eine Flankenerkennung zu ermöglichen. Damit erkennt der Sketch nicht einfach, ob der Button gedrückt ist, sondern ob der Button neu gedrückt wurde.

    • Poti ist ein Wert eines Potentiometers (Drehregler), der später für dynamische Einstellungen genutzt wird.

    • lastPotiVal dient zum Vergleichen alter und neuer Potentiometerwerte, um nur bei tatsächlicher Veränderung zu reagieren.

    Zusätzlich gibt es einige Parameter für die Grafik:

    • bgTrans: Die Transparenz des Hintergrundes für einen leichten Verwischeffekt.

    • maxspeed: Die maximale Geschwindigkeit, die ein Partikel erreichen darf.

    Weitere Variablen steuern das Verhalten des Flowfields:

    • zspeed, yrand, xrand: Sie bestimmen, wie schnell und auf welche Weise sich das Vektorfeld verändert.

    • vectrans: Transparenz der eingezeichneten Vektoren.

    • sele: Ein Zähler, mit dem der Nutzer auswählen kann, welche Eigenschaft gerade mit dem Potentiometer gesteuert wird.

    Abschließend:

    • inc legt fest, in welchen Abständen das Perlin Noise Raster aufgeteilt wird.

    • scl ist die Skalierung der Rasterzellen.

    • cols und rows bestimmen, wie viele Zellen das Raster horizontal und vertikal umfasst.

    2. Spatial Grid: Optimierung der Partikelsuche

    let grid = {}; // Spatial Grid initialisiert
    let gridSize = scl; // Zellenauflösung für Spatial Grid
    
    function getGridIndex(x, y) { //Grid-Index-Berechnung durch Auflösung
      return floor(x / gridSize) + ',' + floor(y / gridSize);
    }

    Ein einfaches, aber effektives Spatial Grid wird vorbereitet:

    • Mit der Funktion getGridIndex(x, y) wird für jede Partikelposition ein Schlüssel (“x,y”) erzeugt. So lassen sich Partikel effizient in Gruppen organisieren.

    3. Setup: Initialisierung der Szene

    function setup() {
      createCanvas(1920, 1080); //HG erstellen
      cols = floor(width / scl); //senkrechten des Rasters
      rows = floor(height / scl);//waagrechten des Rasters
      flowfield = new Array(cols * rows); //erstellt eine eindimensionales array mit dem die rasterwerte gespeichert werden.
    
      for (let i = 0; i < 10000; i++) { //Erstellt X Partikel 
        particles[i] = new Particle();//an position i 0-299 in der 1 dimensionalen Matrix
      }
      serial = new p5.SerialPort();          // Serialport-Objekt erstellen
      serial.on('connected', serverConnected); // Event: Verbunden mit Server
      serial.on('list', gotList);              // Event: Liste der Ports
      serial.on('data', gotData);              // Event: Daten empfangen
      serial.on('error', gotError);            // Event: Fehlerbehandlung
      serial.list();                           // Fordert Liste der Ports an
      // WICHTIG: Passe diesen Portnamen an den an den dur in P5JsSerial Controll siehst!
      serial.open('/dev/tty.usbserial-110');    // z.B. "COM3" unter Windows, "/dev/ttyACM0" unter Linux
      background(0);//HG wird nur einmal geladen
    }

    In der setup()-Funktion passiert Folgendes:

    1. Canvas: Ein Zeichenbereich von 1920x1080 Pixeln wird erzeugt.

    2. Rastergrößen: cols und rows werden passend zum Canvas und zur Rastergröße berechnet.

    3. Flowfield: Ein Array wird angelegt, das für jede Rasterzelle ein Vektorobjekt speichert.

    4. Partikel: Es werden 1000 Partikel erstellt und in ein Array eingefügt.

    5. Serielle Verbindung: Die p5.SerialPort-Bibliothek wird genutzt, um:

      • Den Port zu verbinden,

      • Die verfügbaren Ports aufzulisten,

      • Eingehende Daten zu verarbeiten,

      • Fehler zu behandeln.

      • Ein bestimmter Port wird geöffnet (wichtig: Muss auf das eigene System angepasst werden).

    6. Hintergrundfarbe: Ein schwarzer Hintergrund wird einmal zu Beginn gesetzt.

    4. Draw: Die kontinuierliche Zeichenfunktion

    function draw() { //wird immer aktualisiert
    background(0,0,0,bgTrans); //Hintergrund mit reduzierter opacity um den HG verlauf zu erstellen
      
       grid = {}; //Grid leeren für neuen Frame
    
    for (let i = 0; i < particles.length; i++) { //füllt das Grid mit Partikeln basierend auf Position
      let p = particles[i];
      let index = getGridIndex(p.pos.x, p.pos.y);
      if (!grid[index]) {
        grid[index] = [];
      }
      grid[index].push(p);
    }
     
      
      
    // === Selektion indem sle immer wieder erhöht wird: Flankenerkennung für Buttons ===
    if (B1 === 1 && lastB1 === 0) {
      sele++;
    }
    
    if (B2 === 1 && lastB2 === 0) {
      sele--;
    }
      //selektorvariable rotiert durch beim klicken
    if(sele>=8)
    {sele=0}   
    if(sele<0)
    {sele=7}
    
      
      //wertezuweisung von Poti zu subwerten um in Code einzelne Dinge zu steuern
      if (abs(Poti - lastPotiVal) > 10) {
      if(sele==0){zspeed=Poti/1000000} //zspeed wird bei wert 0 angewählt
      if(sele==1){yrand=Poti/100000} //y Randomisierung wird bei 1 angewählt
      if(sele==2){xrand=Poti/100000} //x Randomisierung wird bei 2 angewählt
      if(sele==3){vectrans=Poti/1023*255}
      if(sele==4){bgTrans=Poti/1023*2/3*254+1}
      if (sele == 5) { //Gridgrößenänderung
      let raw = Poti / 1023 * 200 + 10; 
      // Poti wert auf einen Bereich von 10 bis 210  
    
      let bestScl = 10;
      // Startwert für die beste gefundene Skalierung, wird später überschrieben
    
      for (let i = 10; i <= 200; i++) {
        // Schleife über mögliche Skalierungswerte (10 bis 200)
    
        if (width % i === 0 && height % i === 0) {
          // Prüfen, ob die aktuelle Skalierung `i` das Canvas ohne Rest unterteilen kann (also perfekt reinpasst)
    
          if (abs(i - raw) < abs(bestScl - raw)) {
            // Falls diese Skalierung `i` näher am gewünschten Poti-Wert liegt als der bisherige `bestScl`:
    
            bestScl = i;
            // dann aktualisieren wir den besten Wert mit `i`
          }
        }
      }
    
      scl = bestScl;
      // Setzen der endgültigen Skalierung basierend auf der besten Übereinstimmung
    
      cols = width / scl;
      rows = height / scl;
      // Berechnen, wie viele Spalten (cols) und Reihen (rows) ins Canvas passen
    
      flowfield = new Array(cols * rows);
      // Neuinitialisierung des Flowfields mit genau der passenden Größe für das neue Raster
    }//Gridgrößenänderung
    
      if(sele==6){maxspeed=Poti/1023*6}//Geschwindigkeitsänderung der Partikel
      
      if (sele == 7) {
      newCount = floor(map(Poti, 0, 1023, 100, 10000));
      let diff = newCount - particles.length;
    
      if (diff > 0) {
        for (let i = 0; i < diff; i++) {
          particles.push(new Particle());
        }
      } else if (diff < 0) {
        particles.splice(diff); // entfernt die letzten Partikel
      }
    } //Partikelmenge
    
        
      lastPotiVal = Poti;
      }
    // Letzten Button-Zustand merken
    lastB1 = B1;
    lastB2 = B2;
      
      
      
      
      
      
      
      let yoff = 0;//deklarierung von yoff für perlin noise
      for (let y = 0; y < rows; y++) {//randomisierung für y
        let xoff = 0;//deklarierung von xoff für perlin noise
        for (let x = 0; x < cols; x++) { //randomisierung von X
          let index = x + y * cols;
          let angle = noise(xoff, yoff, zoff) *TWO_PI*8 ;//randomisiert die bewegungen mit einem Pearl noise generator 
          //let angle =random (0,360);
          let v = p5.Vector.fromAngle(angle); //erstellt Vector
          v.setMag(3);//locked die stärke der Vektoren
          flowfield[index] = v;//aufbau des flowfieldes v ist immer die Position
    
          stroke(100,100,100,vectrans);//Vektorenvisualisierungs
          push();//alles was hiernach kommt wird nicht visualisiert
          translate(x * scl, y * scl);	//Verschiebt das Koordinatensystem zur Position der aktuellen Zelle im   Gitter.
          rotate(v.heading());//Ändert die richtung des Vektorpfeiles durch koordinatensystemdrehung
          line(0, 0, scl, 0);//Beschreibt die Richtung der Vektorvisualisierungslinie
         pop();// ab hier ist alles wieder sichtbar
    
          xoff += xrand; // Änderungsfaktor zwischen den waagrechten, wie viel unterschied haben sie untereinander
        }
        yoff += yrand;//wie bei xoff bloß für senkrechten die randomisierungshärte ist durch inc immer gleich
      }
    
      zoff += zspeed; //Zeitliche geschwindigkeit der vektoren
    
      for (let i = 0; i < particles.length; i++) {// Steuerung der Partikel durch flowfield
        particles[i].follow(flowfield);//richtungsänderung flowfield--> partikel
        particles[i].update();//updaten der werte in das partikel
        particles[i].edges();//edgekollision wo spawnt das partikel nachdems raus ausm canvas fliegt
       particles[i].show(); // Zeichnet die neue partikelposition
     }
      // === UI: Anzeige der Input-Werte unten rechts ===
    // --- UI Anzeige am Ende von draw() ---
    // Hintergrund für die UI
    let uiPadding = 10;
    let uiW = 300;
    let uiH = 30;
    let uiX = 0  // <-- Unten rechts: X-Position ans rechte Ende angepasst
    let uiY = height - uiH - 10; // unten am Canvas
    
    noStroke();
    fill(0); // schwarzer Hintergrund
    rect(uiX, uiY, uiW, uiH, 8); // Hintergrundbox mit runden Ecken
    
    // Modusnamen bestimmen
    let aktuellerModus = "";
    let aktuellerWert = 0;
    
    // Bestimmen, was angezeigt werden soll
    if (sele == 0) {
      aktuellerModus = "Z-Speed";
      aktuellerWert = map(zspeed, 0, 0.001, 0, 100); // angepasst an typische Werte
    } else if (sele == 1) {
      aktuellerModus = "Y-Random";
      aktuellerWert = map(yrand, 0, 0.008, 0, 100);
    } else if (sele == 2) {
      aktuellerModus = "X-Random";
      aktuellerWert = map(xrand, 0, 0.008, 0, 100);
    } else if (sele == 3) {
      aktuellerModus = "Vector-Transp.";
      aktuellerWert = map(vectrans, 0, 255, 0, 100);
    } else if (sele == 4) {
      aktuellerModus = "BG-Transp.";
      aktuellerWert = map(bgTrans, 0, 170, 0, 100); // angepasst
    } else if (sele == 5) {
      aktuellerModus = "Grid Size";
      aktuellerWert = map(scl, 200, 10, 0, 100);
    } else if (sele == 6) {
      aktuellerModus = "Max Speed";
      aktuellerWert = map(maxspeed, 0, 6, 0, 100);
    } else if (sele == 7) {
      aktuellerModus = "Particle Count";
      aktuellerWert = map(particles.length, 100, 10000, 0, 100);
    }
    
    aktuellerWert = constrain(aktuellerWert, 0, 100); // Sicherstellen: immer 0-100
    
    // Text zeichnen
    fill(255); // weiße Schrift
    textSize(14);
    textAlign(LEFT, CENTER);
    text("Modus: \"" + aktuellerModus + "\": " + int(aktuellerWert) + "%", uiX + uiPadding, uiY + uiH/2);
    }

    Die draw()-Funktion läuft in einer Schleife und sorgt für die Animation und damit das alles sichtbar wird. Sie ist der Hauptteil des Codes in dem nahezu alle wichtigen Abläufe enthalten sind

    Die wichtigsten Bausteine in der draw()-Funktion kurz erklärt

    4.1 Hintergrund mit Transparenz

    background(0, 0, 0, bgTrans);

    Jeder Frame startet mit einem halbtransparenten schwarzen Hintergrund:

    Dadurch verwischen die Bewegungen der Partikel und erzeugen einen „Schweif-Effekt“ da bei jedem Zyklus ein "schwarzer Schleier" über das vorherige Bild gelegt wird und damit die darunter liegenden Bilder zu verblassen beginnen.

    4.2 Spatial Grid aktualisieren

    grid = {};
    
    for (let i = 0; i < particles.length; i++) {
      let index = getGridIndex(particles[i].pos.x, particles[i].pos.y);
      if (!grid[index]) {
        grid[index] = [];
      }
      grid[index].push(particles[i]);
    }

    Das Grid wird für jeden Frame geleert und neu aufgebaut.

    Jedes Partikel wird anhand seiner aktuellen Position einer Rasterzelle zugeordnet.

    4.3 Buttons und Selektorlogik

    if (B1 == 1 && lastB1 == 0) {
      sele++;
      if (sele > 7) sele = 0;
    }
    if (B2 == 1 && lastB2 == 0) {
      sele--;
      if (sele < 0) sele = 7;
    }
    lastB1 = B1;
    lastB2 = B2;

    Mit den beiden Buttons kann der Benutzer den aktuellen „Selektor“ hoch- oder runterzählen.

    Der Selektor bestimmt, welche Eigenschaft der Potentiometer steuern soll.

    • Button 1 (B1): Selektor wird hochgezählt.

    • Button 2 (B2): Selektor wird heruntergezählt.

    • Der Selektor springt von 7 wieder auf 0 und umgekehrt (Ringstruktur).

    Flankenerkennung:

    Es wird nicht einfach geprüft, ob der Button gedrückt ist, sondern ob er gerade eben gedrückt wurde – also der Übergang von nicht gedrückt zu gedrückt.

    Das vermeidet, dass beim Gedrückthalten des Buttons der Selektor sofort mehrfach weiterzählt.

    Erst beim echten Flankenwechsel (0 → 1) wird eine Aktion ausgelöst.

    Kurz gesagt: Es wird nur auf die steigende Flanke (von 0 auf 1) reagiert.

    4.4 Potentiometer: Einstellungen ändern

    if (abs(Poti - lastPotiVal) > 10) {
      switch (sele) {
        case 0:
          zspeed = map(Poti, 0, 1023, 0, 0.01);
          break;
        case 1:
          yrand = map(Poti, 0, 1023, 0, 10);
          break;
        case 2:
          xrand = map(Poti, 0, 1023, 0, 10);
          break;
        case 3:
          vectrans = map(Poti, 0, 1023, 0, 255);
          break;
        case 4:
          bgTrans = map(Poti, 0, 1023, 0, 255);
          break;
        case 5:
          scl = int(map(Poti, 0, 1023, 10, 100));
          cols = floor(width / scl);
          rows = floor(height / scl);
          flowfield = new Array(cols * rows);
          break;
        case 6:
          maxspeed = map(Poti, 0, 1023, 0.5, 10);
          break;
        case 7:
          let newCount = int(map(Poti, 0, 1023, 100, 3000));
          if (newCount > particles.length) {
            for (let i = particles.length; i < newCount; i++) {
              particles.push(new Particle());
            }
          } else {
            particles.splice(newCount);
          }
          break;
      }
      lastPotiVal = Poti;
    }

    Wenn sich der Wert des Potentiometers deutlich verändert (mehr als 10 Punkte Unterschied), wird geprüft, welcher Modus gerade aktiv ist.

    Je nach Selektor-Einstellung beeinflusst der Potentiometer:

    Selektor (sele) Einstellung Wirkung
    0 zspeed Geschwindigkeit des Flowfield- Zeitverlaufs
    1 yrand Variabilität der Vektoren auf der Y-Achse
    2 xrand Variabilität der Vektoren auf der X-Achse
    3 vectrans Transparenz der gezeichneten Vektoren
    4 bgTrans Transparenz des Hintergrundes
    5 scl + cols, rows Rastergröße des Flowfields (neu berechnen)
    6 maxspeed Maximale Partikelgeschwindigkeit
    7 Partikelanzahl (particles) Anzahl der aktiven Partikel im System

    4.5 Aktualisieren der Flowfield-Vektoren

    let yoff = 0;
    for (let y = 0; y < rows; y++) {
      let xoff = 0;
      for (let x = 0; x < cols; x++) {
        let index = x + y * cols;
        let angle = noise(xoff, yoff, zoff) * TWO_PI * 4;
        let v = p5.Vector.fromAngle(angle);
        v.setMag(1);
        flowfield[index] = v;
        xoff += 0.1 * xrand;
        
        push();
        stroke(255, vectrans);
        strokeWeight(1);
        translate(x * scl, y * scl);
        rotate(v.heading());
        line(0, 0, scl, 0);
        pop();
      }
      yoff += 0.1 * yrand;
    }
    zoff += zspeed;

    Das Flowfield selbst wird dynamisch neu berechnet:

    • Für jede Rasterzelle wird ein Vektor bestimmt.

    • Der Winkel der Vektoren basiert auf Perlin Noise und verändert sich sanft über die Zeit.

    • Die Vektoren werden gezeichnet, indem das Koordinatensystem verschoben und gedreht wird.

    Dies erzeugt ein fließendes, natürliches Muster aus Pfeilen, das sich permanent verändert.

    4.6 Partikel aktualisieren

    for (let i = 0; i < particles.length; i++) {
      particles[i].follow(flowfield);
      particles[i].update();
      particles[i].edges();
      particles[i].show();
    }

    Anschließend werden alle Partikel verarbeitet:

    • Sie folgen dem nächstgelegenen Vektor im Flowfield.

    • Ihre Positionen werden aktualisiert.

    • Wenn sie das Canvas verlassen, tauchen sie auf der gegenüberliegenden Seite wieder auf (Wrap-Around).

    • Schließlich wird jedes Partikel als Punkt oder Linie auf das Canvas gezeichnet.

    4.6.1 Methode follow(vectors)

    follow(vectors) {
      let x = floor(this.pos.x / scl);
      let y = floor(this.pos.y / scl);
      let index = x + y * cols;
      let force = vectors[index];
      this.applyForce(force);
    }

    Diese Methode steuert die Bewegungsrichtung des Partikels:

    • Die aktuelle Position (pos) wird in das Raster (Grid) übersetzt.

    • Daraus wird ein Index berechnet, um die Richtung (force) aus dem Flowfield zu finden.

    • Die gefundene Kraft wird auf das Partikel angewendet.

    4.6.2 Methode applyForce(force)

    applyForce(force) {
      this.acc.add(force);
    }

    Diese Methode addiert eine Kraft (force) zur Beschleunigung (acc) des Partikels.

    So kann das Partikel seine Richtung und Geschwindigkeit ändern.

    4.6.3 Methode update()

    update() { 
      this.vel.add(this.acc);
      this.vel.limit(maxspeed);
      this.pos.add(this.vel);
      this.acc.mult(0);
    }

    In jedem Frame wird die Bewegung des Partikels aktualisiert:

    • Beschleunigung (acc) wird zur Geschwindigkeit (vel) addiert.

    • Geschwindigkeit wird auf die maximale Geschwindigkeit begrenzt.

    • Position wird entsprechend der Geschwindigkeit verändert.

    • Wichtig: Danach wird die Beschleunigung wieder auf 0 gesetzt, damit sie sich nicht aufsummiert. passiert dies nicht würden sich die Partikel auf dauer nicht mehr am Flowfield orientieren.

    4.6.4 Methode show()

    show() {//Partikel wird Visualisiert
        stroke(255,55,20, 100); //Farbe
        strokeWeight(3);//Dicke
        point(this.pos.x, this.pos.y);
      }

    Hier wird das Partikel gezeichnet:

    • Es bekommt eine Farbe (helles Orange) und eine Dicke von 3 Pixeln.

    • Gezeichnet wird ein Punkt an der aktuellen Position (pos).

    4.6.5 Methode edges()

    edges() {
      if (this.pos.x > width) {
        this.pos.x = 0;
        this.pos.y = random(height);
      }
      if (this.pos.x < 0) {
        this.pos.x = width;
        this.pos.y = random(height);
      }
      if (this.pos.y > height) {
        this.pos.y = 0;
        this.pos.x = random(width);
      }
      if (this.pos.y < 0) {
        this.pos.y = height;
        this.pos.x = random(width);
      }
    }

    Diese Methode sorgt dafür, dass Partikel am Rand des Canvas nicht verschwinden:

    • Wenn ein Partikel über den Rand hinausläuft, wird es auf die gegenüberliegende Seite teleportiert.

    • Dabei wird seine Position leicht zufällig verschoben, um das Bild lebendiger zu machen um "Partikelklumpen" aufzulösen, dies ist zusammen mit der avoidClumping(grid) Methode essenziell da sonst sich die Partikel auf Dauer auf einem Fleck ansammeln würden.

    4.6.6 Methode avoidClumping(grid)

    avoidClumping(grid) { 
      let x = floor(this.pos.x / gridSize);
      let y = floor(this.pos.y / gridSize);
      let index = x + ',' + y;
      let cell = grid[index];
    
      if (cell && cell.length > 5) {
        let center = createVector(0, 0);
        for (let other of cell) {
          center.add(other.pos);
        }
        center.div(cell.length);
        
        let away = p5.Vector.sub(this.pos, center);
        away.setMag(0.1);
        this.applyForce(away);
      }
    }

    Diese Methode verhindert, dass Partikel zu sehr verklumpen (“clumping”):

    • Sie prüft in einem Raster (grid), wie viele Partikel in der Nähe sind.

    • Wenn zu viele Partikel in einer Zelle sind, wird ein kleiner Abstoßungsvektor vom Zellzentrum berechnet.

    • Das Partikel wird dadurch sanft weggeschoben.

    4.7 UI: Benutzeroberfläche anzeigen

    push();
    fill(0, 200);
    stroke(255);
    rect(width - 150, height - 50, 140, 40);
    noStroke();
    fill(255);
    textSize(12);
    textAlign(LEFT, CENTER);
    text("Selektor: " + sele, width - 140, height - 40);
    text("Wert: " + int(map(Poti, 0, 1023, 0, 100)) + "%", width - 140, height - 20);
    pop();

    Am unteren rechten Rand wird eine kleine Infobox eingeblendet:

    • Dort steht, welcher Modus gerade aktiv ist (z. B. “Z-Speed”, “Y-Random”, “Partikelanzahl” etc.).

    • Außerdem wird der aktuelle Wert (prozentual angepasst) angezeigt.

    • Die UI besteht aus einem gefüllten Rechteck und Text in Weiß.

    Steuermodul:

    1. Hardware:

    Um den Code zu steuern ist folgende Hardware notwendig:

    • 1x Arduino (Modell beliebig solange 2 Digital Pins und 1 Analog Pin verfügbar)

    • 1x Potentiometer

    • 2x Button

    • 1xBreadboard

    • 5x Leitung Rot

    • 5x Leitung Schwarz

    • 1x Leitung Grün

    diese schließt man nach folgendem Schaltplan an:

    2. Software:

    Der Arduino braucht eine Programmierung die dafür sorgt das dieser die Eingaben über die Poti und Buttons erkennt und diese zuverlässig an den P5Js Code übergibt.

    2.1 Deklaration der Variablen

    const int buttonlinks = 11;
    const int buttonrechts = 10;
    const int poti = A1;

    • buttonlinks: Die Konstante buttonlinks wird auf Pin 11 gesetzt. Hier wird ein Button angeschlossen, der die „linke“ Eingabe darstellt.

    • buttonrechts: Die Konstante buttonrechts wird auf Pin 10 gesetzt. Hier wird ein Button angeschlossen, der die „rechte“ Eingabe darstellt.

    • poti: Die Konstante poti verweist auf den Pin A1 (ein analoger Eingang). Hier wird ein Potentiometer angeschlossen, das analoge Werte liefert.

    2.2 Setup-Funktion

    void setup() {
      pinMode(buttonlinks, INPUT_PULLUP);
      pinMode(buttonrechts, INPUT_PULLUP);
      Serial.begin(9600);
    }

    • pinMode(buttonlinks, INPUT_PULLUP);: Setzt den Pin buttonlinks (Pin 11) als Eingang mit internem Pull-Up-Widerstand. Das bedeutet, dass der Pin standardmäßig auf HIGH gezogen wird, und der Wert wird auf LOW fallen, wenn der Button gedrückt wird.

    • pinMode(buttonrechts, INPUT_PULLUP);: Setzt den Pin buttonrechts (Pin 10) ebenfalls als Eingang mit internem Pull-Up-Widerstand, ähnlich wie bei buttonlinks.

    • Serial.begin(9600);: Startet die serielle Kommunikation mit einer Baudrate von 9600. Dies ist erforderlich, um Daten an den seriellen Monitor zu senden.

    2.3 Loop-Funktion

    void loop() {
      int left = digitalRead(buttonlinks);
      int right = digitalRead(buttonrechts);
      int analogVal = analogRead(poti);

    • int left = digitalRead(buttonlinks);: Liest den Zustand des buttonlinks-Pins (digitaler Eingang). Wenn der Button nicht gedrückt ist, wird der Wert HIGH zurückgegeben, andernfalls LOW.

    • int right = digitalRead(buttonrechts);: Liest den Zustand des buttonrechts-Pins (digitaler Eingang), ähnlich wie bei left.

    • int analogVal = analogRead(poti);: Liest den Wert des Potentiometers an Pin A1. Da es sich um einen analogen Eingang handelt, gibt analogRead() einen Wert zwischen 0 und 1023 zurück (je nach Spannung an dem Pin).

    2.4 Daten ausgeben

    Serial.print(left);
      Serial.print(",");
      Serial.print(right);
      Serial.print(",");
      Serial.println(analogVal);

    • Serial.print(left);: Gibt den Wert von left (den Zustand des linken Buttons) auf dem seriellen Monitor aus.

    • Serial.print(",");: Gibt ein Komma als Trennzeichen zwischen den Werten aus.

    • Serial.print(right);: Gibt den Wert von right (den Zustand des rechten Buttons) aus.

    • Serial.print(",");: Ein weiteres Komma als Trennzeichen.

    • Serial.println(analogVal);: Gibt den Wert von analogVal (den Wert des Potentiometers) aus und fügt am Ende einen Zeilenumbruch hinzu, sodass die nächste Ausgabe in einer neuen Zeile beginnt.

    2.5 Verzögerung

    delay(100); // 10x pro Sekunde

    • delay(100);: Verzögert den Code für 100 Millisekunden. Das bedeutet, dass der Code 10 Mal pro Sekunde durchlaufen wird (1000 ms / 100 ms = 10). Diese Verzögerung sorgt dafür, dass die seriellen Ausgaben nicht zu schnell hintereinander kommen und die Verbindung entlastet wird.

    Zusammenfassung:

    • Der Code liest kontinuierlich die Zustände von zwei Tasten und den Wert eines Potentiometers ein.

    • Diese Werte werden im CSV-Format (Comma-Separated Values) an den seriellen Monitor ausgegeben, wobei jeder Datensatz aus den Zuständen der beiden Tasten und dem Wert des Potentiometers besteht.

    • Der Code läuft ständig und gibt die Daten alle 100 ms aus, was einer Aktualisierungsrate von 10 Ausgaben pro Sekunde entspricht.

    3. Datenübergabe mit P5Js SerialControl

    Um die Ausgegebenen Daten an den Hauptcode zu übergeben braucht es die Software p5.js SerialControl die die Daten einliest und übergibt.

    In P5Js SerialControl muss man nun den Arduino als Serielles Gerät unter "Connect" suchen und auf "open" drücken. In unserem Fall ist dies das Device usbserial-110.

    Nun sollte sich ein Control Panel öffnen das sieht dann so aus:

    Wenn man nun sehen will ob Daten ankommen, kann man "console enabled" und "read in ASCII" aktivieren.

    Nun sollten für die Buttons die werte in 1 oder 0 und das Poti in Werten von 0-1023 ausgegeben werden.

    Damit diese Werte nun noch im Code ankommen können muss man im P5.js Projekt in der HTML Datei die Serial Control Library einfügen. Die Indexseite sollte nun so aussehen:

    <!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>
        <link rel="stylesheet" type="text/css" href="style.css">
        <meta charset="utf-8" />
    <script language="javascript" type="text/javascript" src="https://cdn.jsdelivr.net/npm/p5.serialserver@0.0.28/lib/p5.serialport.js"></script>  </head>
      <body>
        <main>
        </main>
        <script src="sketch.js"></script>
      </body>
    </html>
    Jetzt kannst du mit deinem Controller dein Flowfield kontrollieren und mit diesem Experimentieren. Viel Spaß damit.

    Alle Projektcodes:

    Java Script:

    let serial;              // Serialport-Objekt
    let B1 = 0;              // linker Button
    let B2 = 0;              // rechter Button
    let lastB1 = 0;          //Flankenerkennung
    let lastB2 = 0;          //Flankenerkennung
    let Poti = 0;            // Potentiometer
    let lastPotiVal = 0; //Potiprüfvariable
    let bgTrans=50;
    let maxspeed= 0.5;
    
    let zspeed=0.00005;            // Zgeschwindigkeitsfaktor perlin
    let yrand=0.005;             //Perlin Y randomisator
    let xrand=0.005;             //Perlin X ranomisator 
    let vectrans=0;            // Vectortransparenzwert
    let sele=0;              //selektorvariable
    
    
    
    let inc = 0.01;//variable für X Y Z der Vektoren
    let scl =60; //Skalierung des Vektorenraster
    let cols, rows; //Estellung Variablen für Raster
    
    
    
    
    let zoff = 0; //Geschwindigkeitsvariable erstellt
    let particles = []; //Array für Partikel in dem die nacher gespeichert werden
    let flowfield; //Flowfield als Variable erstellt
    
    let grid = {}; // Spatial Grid initialisiert
    let gridSize = scl; // Zellenauflösung für Spatial Grid
    
    function getGridIndex(x, y) { //Grid-Index-Berechnung durch Auflösung
      return floor(x / gridSize) + ',' + floor(y / gridSize);
    }
    
    function setup() {
      createCanvas(1920, 1080); //HG erstellen
      cols = floor(width / scl); //senkrechten des Rasters
      rows = floor(height / scl);//waagrechten des Rasters
      flowfield = new Array(cols * rows); //erstellt eine eindimensionales array mit dem die rasterwerte gespeichert werden.
    
      for (let i = 0; i < 10000; i++) { //Erstellt X Partikel 
        particles[i] = new Particle();//an position i 0-299 in der 1 dimensionalen Matrix
      }
      serial = new p5.SerialPort();          // Serialport-Objekt erstellen
      serial.on('connected', serverConnected); // Event: Verbunden mit Server
      serial.on('list', gotList);              // Event: Liste der Ports
      serial.on('data', gotData);              // Event: Daten empfangen
      serial.on('error', gotError);            // Event: Fehlerbehandlung
      serial.list();                           // Fordert Liste der Ports an
      // ⚠️ WICHTIG: Passe diesen Portnamen an!
      serial.open('/dev/tty.usbserial-110');    // z.B. "COM3" unter Windows, "/dev/ttyACM0" unter Linux
      
    
    
      background(0);//HG wird nur einmal geladen
    }
    
    function draw() { //wird immer aktualisiert
    background(0,0,0,bgTrans); //Hintergrund mit reduzierter opacity um den HG verlauf zu erstellen
      
       grid = {}; //Grid leeren für neuen Frame
    
    for (let i = 0; i < particles.length; i++) { //füllt das Grid mit Partikeln basierend auf Position
      let p = particles[i];
      let index = getGridIndex(p.pos.x, p.pos.y);
      if (!grid[index]) {
        grid[index] = [];
      }
      grid[index].push(p);
    }
     
      
      
    // === Selektion indem sle immer wieder erhöht wird: Flankenerkennung für Buttons ===
    if (B1 === 1 && lastB1 === 0) {
      sele++;
    }
    
    if (B2 === 1 && lastB2 === 0) {
      sele--;
    }
      //selektorvariable rotiert durch beim klicken
    if(sele>=8)
    {sele=0}   
    if(sele<0)
    {sele=7}
    
      
      //wertezuweisung von Poti zu subwerten um in Code einzelne Dinge zu steuern
      if (abs(Poti - lastPotiVal) > 10) {
      if(sele==0){zspeed=Poti/1000000} //zspeed wird bei wert 0 angewählt
      if(sele==1){yrand=Poti/100000} //y Randomisierung wird bei 1 angewählt
      if(sele==2){xrand=Poti/100000} //x Randomisierung wird bei 2 angewählt
      if(sele==3){vectrans=Poti/1023*255}
      if(sele==4){bgTrans=Poti/1023*2/3*254+1}
      if (sele == 5) { //Gridgrößenänderung
      let raw = Poti / 1023 * 200 + 10; 
      // Poti wert auf einen Bereich von 10 bis 210  
    
      let bestScl = 10;
      // Startwert für die beste gefundene Skalierung, wird später überschrieben
    
      for (let i = 10; i <= 200; i++) {
        // Schleife über mögliche Skalierungswerte (10 bis 200)
    
        if (width % i === 0 && height % i === 0) {
          // Prüfen, ob die aktuelle Skalierung `i` das Canvas ohne Rest unterteilen kann (also perfekt reinpasst)
    
          if (abs(i - raw) < abs(bestScl - raw)) {
            // Falls diese Skalierung `i` näher am gewünschten Poti-Wert liegt als der bisherige `bestScl`:
    
            bestScl = i;
            // dann aktualisieren wir den besten Wert mit `i`
          }
        }
      }
    
      scl = bestScl;
      // Setzen der endgültigen Skalierung basierend auf der besten Übereinstimmung
    
      cols = width / scl;
      rows = height / scl;
      // Berechnen, wie viele Spalten (cols) und Reihen (rows) ins Canvas passen
    
      flowfield = new Array(cols * rows);
      // Neuinitialisierung des Flowfields mit genau der passenden Größe für das neue Raster
    }//Gridgrößenänderung
    
      if(sele==6){maxspeed=Poti/1023*6}//Geschwindigkeitsänderung der Partikel
      
      if (sele == 7) {
      newCount = floor(map(Poti, 0, 1023, 100, 10000));
      let diff = newCount - particles.length;
    
      if (diff > 0) {
        for (let i = 0; i < diff; i++) {
          particles.push(new Particle());
        }
      } else if (diff < 0) {
        particles.splice(diff); // entfernt die letzten Partikel
      }
    } //Partikelmenge
    
        
      lastPotiVal = Poti;
      }
    // Letzten Button-Zustand merken
    lastB1 = B1;
    lastB2 = B2;
      
      
      
      
      
      
      
      let yoff = 0;//deklarierung von yoff für perlin noise
      for (let y = 0; y < rows; y++) {//randomisierung für y
        let xoff = 0;//deklarierung von xoff für perlin noise
        for (let x = 0; x < cols; x++) { //randomisierung von X
          let index = x + y * cols;
          let angle = noise(xoff, yoff, zoff) *TWO_PI*8 ;//randomisiert die bewegungen mit einem Pearl noise generator 
          //let angle =random (0,360);
          let v = p5.Vector.fromAngle(angle); //erstellt Vector
          v.setMag(3);//locked die stärke der Vektoren
          flowfield[index] = v;//aufbau des flowfieldes v ist immer die Position
    
          stroke(100,100,100,vectrans);//Vektorenvisualisierungs
          push();//alles was hiernach kommt wird nicht visualisiert
          translate(x * scl, y * scl);	//Verschiebt das Koordinatensystem zur Position der aktuellen Zelle im   Gitter.
          rotate(v.heading());//Ändert die richtung des Vektorpfeiles durch koordinatensystemdrehung
          line(0, 0, scl, 0);//Beschreibt die Richtung der Vektorvisualisierungslinie
         pop();// ab hier ist alles wieder sichtbar
    
          xoff += xrand; // Änderungsfaktor zwischen den waagrechten, wie viel unterschied haben sie untereinander
        }
        yoff += yrand;//wie bei xoff bloß für senkrechten die randomisierungshärte ist durch inc immer gleich
      }
    
      zoff += zspeed; //Zeitliche geschwindigkeit der vektoren
    
      for (let i = 0; i < particles.length; i++) {// Steuerung der Partikel durch flowfield
        particles[i].follow(flowfield);//richtungsänderung flowfield--> partikel
        particles[i].update();//updaten der werte in das partikel
        particles[i].edges();//edgekollision wo spawnt das partikel nachdems raus ausm canvas fliegt
       particles[i].show(); // Zeichnet die neue partikelposition
     }
      // === UI: Anzeige der Input-Werte unten rechts ===
    // --- UI Anzeige am Ende von draw() ---
    // Hintergrund für die UI
    let uiPadding = 10;
    let uiW = 300;
    let uiH = 30;
    let uiX = 0  // <-- Unten rechts: X-Position ans rechte Ende angepasst
    let uiY = height - uiH - 10; // unten am Canvas
    
    noStroke();
    fill(0); // schwarzer Hintergrund
    rect(uiX, uiY, uiW, uiH, 8); // Hintergrundbox mit runden Ecken
    
    // Modusnamen bestimmen
    let aktuellerModus = "";
    let aktuellerWert = 0;
    
    // Bestimmen, was angezeigt werden soll
    if (sele == 0) {
      aktuellerModus = "Z-Speed";
      aktuellerWert = map(zspeed, 0, 0.001, 0, 100); // angepasst an typische Werte
    } else if (sele == 1) {
      aktuellerModus = "Y-Random";
      aktuellerWert = map(yrand, 0, 0.008, 0, 100);
    } else if (sele == 2) {
      aktuellerModus = "X-Random";
      aktuellerWert = map(xrand, 0, 0.008, 0, 100);
    } else if (sele == 3) {
      aktuellerModus = "Vector-Transp.";
      aktuellerWert = map(vectrans, 0, 255, 0, 100);
    } else if (sele == 4) {
      aktuellerModus = "BG-Transp.";
      aktuellerWert = map(bgTrans, 0, 170, 0, 100); // angepasst
    } else if (sele == 5) {
      aktuellerModus = "Grid Size";
      aktuellerWert = map(scl, 200, 10, 0, 100);
    } else if (sele == 6) {
      aktuellerModus = "Max Speed";
      aktuellerWert = map(maxspeed, 0, 6, 0, 100);
    } else if (sele == 7) {
      aktuellerModus = "Particle Count";
      aktuellerWert = map(particles.length, 100, 10000, 0, 100);
    }
    
    aktuellerWert = constrain(aktuellerWert, 0, 100); // Sicherstellen: immer 0-100
    
    // Text zeichnen
    fill(255); // weiße Schrift
    textSize(14);
    textAlign(LEFT, CENTER);
    text("Modus: \"" + aktuellerModus + "\": " + int(aktuellerWert) + "%", uiX + uiPadding, uiY + uiH/2);
    }
    //===Konsolenausgabe für debug
    function serverConnected() {
      print("Connected to serial server");
    }
    
    function gotList(thelist) {
      print("List of Serial Ports:");
      for (let i = 0; i < thelist.length; i++) {
        print(i + ": " + thelist[i]);
      }
    }
    
    function gotError(theerror) {
      print("Serial Error: " + theerror);
    }
    
    function gotData() {
      let currentString = serial.readLine();  // Neue Zeile lesen
      currentString = trim(currentString);    // Whitespace entfernen
      if (!currentString) return;             // Falls leer, abbrechen
    
      let data = currentString.split(",");    // CSV trennen
      if (data.length === 3) {
        B1 = int(data[0]);
        B2 = int(data[1]);
        Poti = int(data[2]);
        // print(`B1: ${B1}, B2: ${B2}, Poti: ${Poti}`); // Debug-Ausgabe
      }
    }
    
    class Particle {  //hier werden die Partikel erstellt
      constructor() {
        this.pos = createVector(random(width), random(height));
        this.vel = createVector(0, 0);
        this.acc = createVector(0,0);
        this.maxspeed = maxspeed;
        
      }
    
      follow(vectors) {//hier wird die Richtung den Partikeln übergeben  abhängig von der Position   
        let x = floor(this.pos.x / scl);
        let y = floor(this.pos.y / scl);
        let index = x + y * cols;
        let force = vectors[index];
        this.applyForce(force);
      }
    
      applyForce(force) { // hier werden die "kräfte" an die PAtikel übergeben
        this.acc.add(force);
      }
    
     update() { 
      this.vel.add(this.acc); // Beschleunigung zur Geschwindigkeit addieren
      this.vel.limit(maxspeed); // Geschwindigkeit auf Maximalwert begrenzen
      this.pos.add(this.vel); // Neue Position berechnen (Position += Geschwindigkeit)
      this.acc.mult(0); // 💥 Wichtig: Beschleunigung nach jedem Frame zurücksetzen
    }
    
      show() {//Partikel wird Visualisiert
        stroke(255,55,20, 100); //Farbe
        strokeWeight(3);//Dicke
        point(this.pos.x, this.pos.y);
      }
    
    edges() {
      if (this.pos.x > width) {
        this.pos.x = 0;
        this.pos.y = random(height);
      }
      if (this.pos.x < 0) {
        this.pos.x = width;
        this.pos.y = random(height);
      }
      if (this.pos.y > height) {
        this.pos.y = 0;
        this.pos.x = random(width);
      }
      if (this.pos.y < 0) {
        this.pos.y = height;
        this.pos.x = random(width);
      }
    }
    avoidClumping(grid) { // Schnelle Zellenbasierte Clumpingvermeidung
        let x = floor(this.pos.x / gridSize);
        let y = floor(this.pos.y / gridSize);
        let index = x + ',' + y;
        let cell = grid[index];
      
        if (cell && cell.length > 5) { // Nur wenn zu viele Partikel in einer Zelle
          let center = createVector(0, 0);
          for (let other of cell) {
            center.add(other.pos);
          }
          center.div(cell.length);
          
          let away = p5.Vector.sub(this.pos, center); // Von Zentrum weg
          away.setMag(0.1); // kleine Kraft reicht
          this.applyForce(away);
        }
      }
    }

    HTML:

    <!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>
        <link rel="stylesheet" type="text/css" href="style.css">
        <meta charset="utf-8" />
    <script language="javascript" type="text/javascript" src="https://cdn.jsdelivr.net/npm/p5.serialserver@0.0.28/lib/p5.serialport.js"></script>  </head>
      <body>
        <main>
        </main>
        <script src="sketch.js"></script>
      </body>
    </html>

    CSS:

    html, body {
      margin: 0;
      padding: 0;
    }
    canvas {
      display: block;
    }

    C++(für Arduino):

    const int buttonlinks = 11;
    const int buttonrechts = 10;
    const int poti = A1;
    
    void setup() {
      pinMode(buttonlinks, INPUT_PULLUP);
      pinMode(buttonrechts, INPUT_PULLUP);
      Serial.begin(9600);
    }
    
    void loop() {
      int left = digitalRead(buttonlinks);
      int right = digitalRead(buttonrechts);
      int analogVal = analogRead(poti);
    
      // Ausgabe im CSV-Format: 1,0,457
      Serial.print(left);
      Serial.print(",");
      Serial.print(right);
      Serial.print(",");
      Serial.println(analogVal);
    
      delay(100); // 10x pro Sekunde
    }

    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.