0t1 steckt noch in den Kinderschuhen.

Pust-Effekt durchs Mikrofon p5.js

In diesem Tutorial wird die p5.js-Bibliothek verwendet, um eine interaktive Anwendung zu erstellen. Dabei wird das Mikrofon genutzt, um eine visuelle Darstellung ähnlich einer Pusteblume zu erzeugen. Die Animation von Pusteblumen wird dabei durch den Klangpegel gesteuert, wobei die Bewegung der Blüten auf Audioeingaben reagiert.

Editor

Um die Anwendung programmieren zu können, öffnen wir zuallererst den p5.js Editor unter folgendem Link: https://editor.p5js.org/.

Grundlegendes

function setup() {
  createCanvas(400, 400);
}

function draw() {
  background(220);
}

Die setup()-Funktion wird einmalig am Anfang des Programms aufgerufen. Die draw()-Funktion hingegen wird in einer Schleife für jeden Frame ständig aktualisiert, um die Animationen zu rendern.

Variablen und Arrays definieren

"use strict";

Die Zeile "use strict"; ist eine sogenannte »Directive« in JavaScript. Sie weist den JavaScript-Interpreter an, den »Strict Mode« für den gesamten Code oder einen bestimmten Code-Block zu aktivieren.

Der Strict Mode ist eine Funktion von JavaScript, die dazu dient, Fehler im Code zu vermeiden und die Sicherheit zu verbessern, indem sie bestimmte problematische Verhaltensweisen unterbindet und strengere Regeln für die Vernwundung von JavaScript einführt.

Änderungen, die der Strict Mode bewirkt sind Folgende:

  • Fehler werden strenger behandelt: Bestimmte Aktionen, die im normalen Modus toleriert werden, lösen im Strict Mode Fehler aus

  • Verbesserte Fehlermeldungen: Fehlermeldungen enthalten oft detailliertere Informationen darüber, was falsch gelaufen ist

  • Verbesserte Sicherheit: Einige potenziell unsichere oder fehleranfällige Aktionen sind im Strict Mode nicht zulässig

In der Regel wird empfohlen, den Strict Mode zu verwenden, um sicherzustellen, dass der JavaScript-Code konsistenter, sicherer und weniger fehleranfällig ist.

let mic, gui, micLevel;
let controller, panel;
let dandelions = [];

In diesem Code-Bereich werden verschiedene Variablen und Arrays, die im weiteren Verlauf des Codes verwendet werden, definiert:

  1. mic, gui, micLevel: Diese Variablen werden verwendet, um das Mikrofon-Input-Objekt, das GUI-Objekt und das aktuelle Mikrofonlevel zu speichern

  2. controller, panel: Diese Variablen werden verwendet, um das GUI-Controller-Objekt und das Panel-Objekt zu speichern, die für die Steuerung der Benutzeroberfläche verwendet werden

  3. dandelions: Dies ist ein Array, das verwendet wird, um alle Dandelion-Objekte (Pusteblumen) zu speichern, die auf dem Bildschirm gezeichnet werden. Jedes Element in diesem Array repräsentiert eine einzelne Pusteblume

Setup-Funktion

function setup() {
  createCanvas(windowWidth, windowHeight);
  dandelions.push(new Dandelion(width / 2, height / 2));

Nun kommen wir zu der setup()-Funktion. Diese wird, wie vorher schon genannt, einmalig am Anfang des Programmes aufgerufen und ist dazu da, um Variablen zu initialisieren, den Canvas und andere Operationen einzurichten, die nur einmal ausgeführt werden müssen.

Am Anfang der setup()-Funktion wird mit createCanvas(windowWidth, windowHeight); ein Canvas-Element erstellt, das die gesamte Breite und Höhe des Browserfensters einnimmt. Die aktuelle Breite und Höhe des Browserfensters wird durch die Funktionen windowWidth und windowHeight geliefert.

In der nächsten Zeile wird ein neues Dandelion-Objekt erstellt und durch die Methode .push dem Array dandelions hinzugefügt. Um nun die horizontale und vertikale Mitte des Canvas zu finden, damit das Objekt in der Mitte des Canvas platziert werden kann, werden width / 2 und height / 2 verwendet.

GUI (Grafische Benutzeroberfläche)

// setup GUI
  controller = function() {
    this.debug = false;
    this.strength = 0.2;
    this.number = 3;
    this.day = true;
  }

Im nächsten Schritt wird eine GUI (Grafische Benutzeroberfläche) eingerichtet. In unserem Fall wird eine GUI-Bibliothek namens dat.GUI verwendet, um eine Benutzeroberfläche zu erstellen, die dem Benutzer ermöglicht, Einstellungen interaktiv ändern zu können.

Der Code initialisiert eine Steuerungsstruktur (controller), die die Einstellungen für die GUI definiert, und erstellt dann eine Instanz dieser Steuerungsstruktur (panel). Anschließend wird die GUI erstellt und jedem Einstellungselement (wie Schaltern, Schiebereglern usw.) ein entsprechendes GUI-Element hinzugefügt.

Die controller-Funktion dient als Konstruktor für ein Objekt, das die Steuerelemente der grafischen Benutzeroberfläche (GUI) enthält. Diese Steuerelemente werden später mit Hilfe der dat.GUI-Bibliothek erstellt und können später von jedem Benutzer individuell verändert werden.

panel = new controller();
  gui = new dat.GUI();
  gui.add(panel, 'debug');
  gui.add(panel, 'strength', 0.2, 0.8);
  gui.add(panel, 'number', 1, 7).step(1);
  gui.add(panel, 'day');

Anschließend wird mit panel = new controller(); ein neues Objekt erstellt, das mit der controller-Funktion initialisiert wird. Dieses Objekt wird verwendet, um die Eigenschaften der Steuerelemente zu speichern.

Als Nächstes wird mit gui = new dat.GUI(); eine Instanz der dat.GUI-Bibliothek erstellt, um die grafischen Benutzeroberflächen-Steuerelemente zu erstellen. Für dieses Tutorial ist diese Bibliothek essenziell. Diese kannst du dir aus der original Quelle dieses Tutorials holen, die unten bei den Quellen verlinkt ist.

Mit gui.add(panel, 'debug'); wird ein Schalter hinzugefügt, mit dem die Eigenschaft debug des panel-Objekts in der grafischen Benutzeroberfläche ein- und ausgeschaltet werden kann.

Der Debug bezieht sich auf den Prozess des Auffindens, Analysierens und Behebens von Fehlern oder Fehlfunktionen innerhalb eines Porgrammes. Mit dieser Funktion ist es daher möglich, während der Ausführung eines Programmes, zusätzliche Informationen zu sammeln. Dies hilft Fehler zu identifizieren und zu verstehen, was im Code passiert.

gui.add(panel, 'strength', 0.2, 0.8); dient dazu, einen Schieberegler hinzuzufügen, der die Möglichkeit bietet, den Wert der strength-Eigenschaft im Bereich von 0,2 bis 0,8 einzustellen.

Die strength-Eigenschaft steuert die Stärke der Kräfte, die auf die Samen der Pusteblume wirken. Diese Kräfte werden durch den Mikrofonpegel (gemessen durch das Mikrofon des Geräts) und eine zufällige Komponente beeinflusst. Je höher der Wert von strength, desto stärker sind die Kräfte, die auf die Samen wirken, was dazu führt, dass eine viel größere Kraft benötigt wird, damit die Samen wegfliegen können.

Außerdem wird noch ein weiterer Schieberegler hinzugefügt, um die Anzahl der Dandelion-Objekte im Bereich von eins bis sieben einzustellen. Das heißt, es können bis zu sieben Pusteblumen gleichzeitig angezeigt werden. Der step(1)-Aufruf in gui.add(panel, 'number', 1, 7).step(1); bedeutet, dass der Schieberegler nur Ganzzahlen annimmt.

Schließlich wird mit gui.add(panel, 'day'); ein Kippschalter hinzugefügt, um zwischen dem Tag- und Nachtmodus zu wechseln. Diese Steuerelemente werden später verwendet, um die Darstellung des Programms zu ändern.

<!DOCTYPE html>
<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/p5.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/addons/p5.dom.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/0.7.3/addons/p5.sound.min.js"></script>
    <script src="dat.gui.min.js"></script>
    <link rel="stylesheet" type="text/css" href="style.css">
    <meta charset="utf-8" />

  </head>
  <body>
    <script src="sketch.js"></script>
  </body>
</html>

Damit die GUI-Datenbank auch funktioniert, muss das HTML etwas verändert werden. Zum einen muss die dat.gui.min.js Datei verlinkt werden und zum anderen müssen drei Links hinzugefügt werden. Nachdem der HTML-Code geändert, eine neue .js-Datei mit dem Namen dat.gui.min.js erstellt und der entsprechende Code in die neue .js-Datei eingefügt wurde, sollte die Benutzeroberfläche nun funktionieren und wie folgt aussehen:

Mikrofon

//Mic einrichten
mic = new p5.AudioIn()
mic.start();

In diesem Teil des Codes werden die Audiobibliothek p5.AudioIn und das Mikrofon initialisiert. Das Mikrofon wird verwendet, um die Bewegung der Dandelions zu beeinflussen.

Mit Hilfe von mic = new p5.AudioIn() wird ein neues p5.AudioIn-Objekt namens mic erstellt. Das p5.AudioIn-Objekt ist Teil der p5.js-Bibliothek und wird verwendet, um Audioeingaben über das Mikrofon des Benutzers zu erfassen.

Durch mic.start(); wird das Mikrofon gestartet. Nach dem Aufruf dieser Methode beginnt das mic-Objekt Audio vom Mikrofon zu erfassen, und stellt es für weitere Verarbeitungsschritte zur Verfügung, wie z. B. die Analyse des Audiopegels oder die Verarbeitung von Echtzeit-Audioeffekten.

Die Animation

function draw() {
  background((panel.day)? 255 : 50);
 
  // get mic
  micLevel = mic.getLevel();
  if (panel.debug) {
    ellipse(width * 0.1, constrain(height - micLevel * height * 5, 0, height), 10, 10);
  }
 
  for (let i = 0; i < dandelions.length; i++) {
    let d = dandelions[i];
    d.applyForce(createVector(micLevel, random(-0.12,0.1)));
    d.display();
  }
 
 
  if (dandelions.length > panel.number) {
    dandelions.shift();
  }
 
}
function doubleClicked() {
  dandelions.push(new Dandelion(mouseX, mouseY));
}

Anschließend wird die draw()-Funktion erstellt. In dieser wird der Hauptteil der Animation ausgeführt. Hier wird der Hintergrund gezeichnet, der Mikrofonpegel abgerufen und verwendet, um die Bewegung der Dandelions zu beeinflussen. Außerdem werden die Dandelions aktualisiert und dargestellt.

Die Zeile background((panel.day)? 255 : 50); ist ein Ausdruck, der in der draw()-Funktion verwendet wird, um den Hintergrund der Leinwand zu zeichnen. Hier wird ein sogenannter »ternärer Operator« verwendet, um zwischen zwei verschiedenen Farbwerten zu wählen, je nachdem, ob panel.day wahr (true) oder falsch (false) ist. Diese Eigenschaft steuert den Tag- und Nachtmodus der Darstellung.

Der aktuelle Pegel wird durch micLevel = mic.getLevel(); abgerufen und in der Variable micLevel gespeichert. Der Pegel wird später verwendet, um die Bewegung der Dandelions zu steuern.

Zudem wurde noch ein Debugger eingebaut. Dieser wird mit Hilfe von if (panel.debug) { ... } überprüft, ob er aktiviert ist. Wenn dies der Fall ist, wird ein kleiner Kreis gezeichnet, der den aktuellen Mikrofonpegel visualisiert und sich entsprechend des Pegels bewegt.

Folgende Schleife for (let i = 0; i < dandelions.length; i++) { ... } durchläuft alle vorhandenen Dandelions und aktualisiert ihre Positionen basierend auf dem Mikrofonpegel.

Mit if (dandelions.length > panel.number) { dandelions.shift(); } wird überprüft, ob die Anzahl der Dandelions größer ist als die im GUI eingestellte maximale Anzahl. Wenn dies der Fall ist, wird das älteste Dandelion-Objekt aus dem Array entfernt.

Um ein weiteres Dandelion an einer beliebigen Stelle hinzuzufügen, muss man auf die Leinwand doppelklicken. Denn dadurch wird die Funktion function doubleClicked() { ... } aufgerufen und dem Array dandelions hinzugefügt.

Bisheriger Code

"use strict";
let mic, gui, micLevel;
let controller, panel;
let dandelions = [];

function setup() {
  createCanvas(windowWidth, windowHeight);
  dandelions.push(new Dandelion(width / 2, height / 2));

  // setup GUI
  controller = function() {
    this.debug = false;
    this.strength = 0.2;
    this.number = 3;
    this.day = true;
  }
  panel = new controller();
  gui = new dat.GUI();
  gui.add(panel, 'debug');
  gui.add(panel, 'strength', 0.2, 0.8);
  gui.add(panel, 'number', 1, 7).step(1);
  gui.add(panel, 'day');

  // setup mic
  mic = new p5.AudioIn()
  mic.start();
}

function draw() {
  background((panel.day)? 255 : 50);

  // get mic
  micLevel = mic.getLevel();
  if (panel.debug) {
    ellipse(width * 0.1, constrain(height - micLevel * height * 5, 0, height), 10, 10);
  }

  for (let i = 0; i < dandelions.length; i++) {
    let d = dandelions[i];
    d.applyForce(createVector(micLevel, random(-0.12,0.1)));
    d.display();
  }


  if (dandelions.length > panel.number) {
    dandelions.shift();
  }

}

function doubleClicked() {
  dandelions.push(new Dandelion(mouseX, mouseY));
}

Class Dandelion

In der Klasse class Dandelion { ... } werden die Eigenschaften und Methoden definiert, die ein einzelnes Dandelion-Objekt repräsentieren. Hier werden Kräfte auf die Samen angewendet und ihre Darstellung entsprechend angepasst.

class Dandelion {
  constructor(x, y) {
    this.pos = createVector(x, y);
    this.rad = random(10, 20);
    this.seeds = [];
    this.seedN = this.rad + 8;

Der Konstruktor wird aufgerufen, wenn ein neues Dandelion-Objekt erstellt wird. Er initialisiert die Eigenschaften des Dandelion-Objekts. Hier werden die Position (this.pos), der Radius (this.rad), das Array der Samen (this.seeds) und die Anzahl der Samen (this.seedN) festgelegt.

  • this.pos = createVector(x, y);: Die Eigenschaft pos speichert die Position des Dandelions im Koordinatensystem

  • this.rad = random(10, 20);: Die Eigenschaft rad speichert den Radius des Dandelions. Hier wird der Radius zufällig zwischen zehn und 20 Pixeln ausgewählt, um jedem Dandelion eine unterschiedliche Größe zu geben

  • this.seeds = [];: Die Eigenschaft seeds ist ein leeres Array, das dazu dient, die Samen des Dandelions zu speichern

  • this.seedN = this.rad + 8;: Die Eigenschaft seedN speichert die Anzahl der Samen, die das Dandelion haben wird. Sie wird berechnet, indem zum Radius des Dandelions (rad) die Zahl acht addiert wird

for (let i = 0; i < this.seedN; i++) {
      let ori = i * 2 * PI / this.seedN + PI / 2;
      let vec = p5.Vector.fromAngle(ori);
      vec.mult(this.rad);
      let hue = random(360, 100);
      this.seeds.push(new Seed(vec.x, vec.y, ori, this.seedN, hue));
    }
  }

In der darauffolgenden Schleife wird jedes Samenobjekt des Dandelions initialisiert und dem Array this.seeds hinzugefügt. Für jeden Samen werden ein zufälliger Winkel (ori), eine Position relativ zur Mitte des Dandelions (vec), und eine zufällige Farbe (hue) generiert. Diese Werte werden verwendet, um neue Seed-Objekte zu erstellen und sie dem Array seeds hinzuzufügen.

  • for (let i = 0; i < this.seedN; i++) { ... } beginnt eine Schleife, die this.seedN-mal durchlaufen wird. this.seedN ist die Anzahl der Samen, die im Dandelion erstellt werden sollen

  • let ori = i * 2 * PI / this.seedN + PI / 2; hierbei wird der Winkel (ori) für den aktuellen Samen berechnet. Der Winkel wird gleichmäßig um den Kreis verteilt, indem der Index i durch die Anzahl der Samen this.seedN geteilt und dann mit 2 * PI multipliziert wird. Durch die Hinzufügung von PI / 2 wird der Startwinkel auf 90 Grad (im Uhrzeigersinn) verschoben.

  • let vec = p5.Vector.fromAngle(ori);: Ein Vektor (vec) wird aus dem Winkel (ori) erstellt. Dieser Vektor repräsentiert die Richtung, in die der Samen zeigen wird.

  • vec.mult(this.rad);: Der Vektor wird mit dem Radius des Dandelions (this.rad) multipliziert, um die Position des Samens relativ zum Zentrum des Dandelions zu bestimmen. Dadurch wird der Samen vom Zentrum des Dandelions aus in die richtige Richtung verschoben.

  • Bei let hue = random(360, 100); wird eine zufällige Farbe (hue) generiert, die später für die Darstellung des Samens verwendet wird. Die Farbe wird zufällig zwischen 360 und 100 gewählt, wobei 100 die maximale Helligkeit ist.

  • this.seeds.push(new Seed(vec.x, vec.y, ori, this.seedN, hue));: Ein neues Seed-Objekt wird erstellt und dem seeds-Array des Dandelions hinzugefügt. Dabei werden die berechnete Position (vec.x, vec.y), der Winkel (ori), die Anzahl der Samen insgesamt (this.seedN) und die zufällige Farbe (hue) übergeben.

Auf diese Weise wird für jeden Samen des Dandelions ein entsprechendes Seed-Objekt initialisiert und dem seeds-Array hinzugefügt.

Hier sind einige Beispiele dafür, wie sich das Ändern des Hue-Werts auswirken könnte:

    Den Zustand der Samen verändern

    applyForce(f) {
        for (let i = 0; i < this.seedN; i++) {
          this.seeds[i].applyForce(createVector(f.x * random(1), f.y + random(-0.1, 0.1)));
          this.seeds[i].update();
        }
      }

    Die applyForce(f) {...} Methode wird verwendet, um eine Kraft (f) auf alle Samen des Dandelions anzuwenden und anschließend den Zustand jedes Samens zu aktualisieren. Sie geht somit durch jedes Seed-Objekt im seeds-Array und wendet die Kraft darauf an, bevor sie die update()-Methode jedes Samens aufruft.

    • Die for (let i = 0; i < this.seedN; i++) { ... } lässt jedes Seed-Objekt im seeds-Array des Dandelions durchlaufen

    • this.seeds[i].applyForce(createVector(f.x * random(1), f.y + random(-0.1, 0.1)));: Für jeden Samen wird die Methode applyForce() aufgerufen. Diese Methode erwartet einen Vektor als Argument, der die auf den Samen wirkende Kraft darstellt. Hier wird ein neuer Vektor erstellt, indem der Vektor f skaliert wird. Der Wert f.x wird mit einem zufälligen Wert zwischen Null und Eins multipliziert, um eine gewisse Variabilität einzuführen, und f.y wird um einen zufälligen Wert zwischen -0.1 und 0.1 verändert. Dieser Vektor repräsentiert die angewendete Kraft auf den Samen.

    • this.seeds[i].update();: Nachdem die Kraft auf den Samen angewendet wurde, wird die Methode update() aufgerufen, um den Zustand des Samens zu aktualisieren. Diese Methode führt Bewegungsberechnungen durch, basierend auf den Kräften, die auf den Samen wirken. Dies ermöglicht es den Samen, sich entsprechend der Umgebung oder der Simulation zu bewegen oder zu verändern.

    Indem diese Schleife durchlaufen wird, wird die auf jeden Samen wirkende Kraft angewendet und dann der Zustand jedes Samens aktualisiert. Dadurch können die Samen des Dandelions auf die Kraft reagieren und sich entsprechend bewegen oder verändern.

    Darstellung auf dem Bildschirm

    display() {
        push();
        translate(this.pos.x, this.pos.y);
        if (panel.day){
        fill(0, 0, 0, 200);
          stroke(0, 0, 0, 100);}
        else {
        fill(255);
        stroke(255);
        }
        ellipse(0,0,this.rad*2);
        strokeWeight(5);
        line(0, this.rad, 0, height);
        for (let i = 0; i < this.seedN; i++) {
          this.seeds[i].display();
        }
        pop();
      }
    }

    Die display()-Methode wird verwendet, um die Pusteblume (Dandelion) und seine Samen auf dem Bildschirm darzustellen. Sie übersetzt den Ursprung des Koordinatensystems zur Position des Dandelions und zeichnet dann einen Kreis für den Kopf des Dandelions und ebenso einen Stängel. Die Farben werden basierend auf der Tageszeit (panel.day) ausgewählt. Dann wird eine Schleife verwendet, um jedes Seed-Objekt im seeds-Array zu durchlaufen. Für jedes dieser Objekte wird die Methode display() aufgerufen, um jeden Samen grafisch darzustellen.

    • push(); speichert den aktuellen Zustand des Zeichenkontexts (z. B. Transformationen wie Verschiebungen oder Rotationen) im Stapel. Dies ermöglicht es, Änderungen vorzunehmen, die später wieder rückgängig gemacht werden können

    • translate(this.pos.x, this.pos.y);: Hier wird der Ursprung des Koordinatensystems zum Mittelpunkt des Dandelions verschoben. Dadurch werden alle nachfolgenden Zeichnungen relativ zum Mittelpunkt des Dandelions durchgeführt

    • Mit if (panel.day) { ... } else { ... } wird überprüft, ob der Tag- oder Nachtmodus aktiv ist. Dies wird durch die Eigenschaft panel.day gesteuert. Abhängig vom Modus wird entweder eine dunkle Farbe für den Kreis am Stängel und den Stängel selbst festgelegt (fill(0, 0, 0, 200) und stroke(0, 0, 0, 100)) oder eine weiße Farbe für den Stängel (fill(255) und stroke(255)). Dunkel für den Tag-Modus, Hell für den Nacht-Modus.

    • ellipse(0, 0, this.rad*2); zeichnet einen Kreis mit einem Durchmesser, der dem doppelten Radius (this.rad) des Dandelions entspricht. Dies repräsentiert den Kopf des Dandelions. Mit strokeWeight(5); wird die Strichstärke auf fünf Pixel festgelegt.

    • line(0, this.rad, 0, height); zeichnet eine Linie vom Mittelpunkt des Dandelions bis zur unteren Grenze des Bildschirms und repräsentiert somit den Stängel des Dandelions.

    • for (let i = 0; i < this.seedN; i++) { this.seeds[i].display(); }: In dieser Schleife wird die display()-Methode jedes Samens des Dandelions aufgerufen, um sie auf dem Bildschirm darzustellen.

    Am Ende der display()-Methode wird pop() verwendet, um das gespeicherte Koordinatensystem wiederherzustellen. Dadurch wird sichergestellt, dass nach dem Zeichnen des Dandelions alle Transformationen nur auf dieses Objekt angewendet wurden und nicht versehentlich auf nachfolgende Zeichnungen einfließen. Das bedeutet somit, dass alle Transformationen, die mit push() angewendet wurden, dadurch rückgängig gemacht werden.

    Class Seed

    Die Seed-Klasse ist dafür verantwortlich, die Eigenschaften und das Verhalten eines einzelnen Samens in der Pusteblume zu definieren. Jeder Samen hat eine Position auf dem Bildschirm, eine Geschwindigkeit und Beschleunigung, die seine Bewegung steuern, sowie eine Größe und Farbe. Die Klasse ermöglicht es auch, eine Kraft auf den Samen auszuüben, um ihn zu bewegen und seine Position und Ausrichtung mit der Zeit zu aktualisieren. Schließlich zeichnet die Klasse den Samen auf den Bildschirm, indem sie einen Kreis mit der entsprechenden Größe und Farbe an seiner Position zeichnet.

    class Seed {
      constructor(x, y, ori, size, hue) {
        this.pos = createVector(x, y);
        this.vel = createVector(); 
        this.acc = createVector();
        this.ori = ori;
        this.oriOri = ori;
        this.size = size;
        this.mass = this.size / 4;
        this.fly = false;
        this.random = random(2);
        this.dir = 3 * PI / 2;
        this.hue = hue;
      }

    Der Konstruktor initialisiert eine Seed-Instanz mit verschiedenen Eigenschaften, die für die Darstellung und Bewegung eines Samens erforderlich sind. Diese Eigenschaften bestehen aus der Anfangsposition (x, y), der Anfangsausrichtung (ori), der Größe (size) und dem Hue-Wert für die Farbe (hue).

    • Durch die Koordinaten x und y wird die Position pos des Samens festgelegt und an die Seed-Klasse übergeben

    • Die Geschwindigkeit vel und Beschleunigung acc des Samens werden als leere Vektoren initialisiert, da der Samen zu Beginn ruht

    • Die aktuelle Ausrichtung ori des Samens wird auf den übergebenen Wert ori gesetzt, der die Richtung angibt, in die der Samen anfangs fliegen wird

    • Die ursprüngliche Ausrichtung oriOri wird ebenfalls auf den übergebenen Wert ori gesetzt und später verwendet, um die Ausrichtung des Samens zu aktualisieren

    • Die Größe des Samens size wird durch den übergebenen Parameter size festgelegt

    • Die Masse des Samens mass wird als ein Viertel seiner Größe festgelegt, was die Physiksimulation beeinflusst

    • Der Flugstatus fly wird auf false gesetzt, da der Samen zu Beginn nicht fliegt

    • Ein zufälliger Faktor random wird generiert, der später für die Variation der Flugrichtung verwendet wird

    • Die Flugrichtung dir wird auf 3 * π / 2 gesetzt, was einer vertikalen Ausrichtung entspricht

    • Der Farbwert hue wird als Argument übergeben und später für die Farbe des Samens verwendet

    Aktualisierung

    update() {
        this.vel.add(this.acc);
        this.pos.add(this.vel);
        this.acc.mult(0);
        if (this.fly) {
          this.ori = lerp(this.ori, this.dir, 0.1);
        }
        this.ori = (this.ori < PI / 2) ? this.ori + PI * 2 : this.ori;
        this.ori = (this.ori > PI * 5 / 2) ? this.ori - PI * 2 : this.ori;
        this.dir = 3 * PI / 2 + this.random;
      }

    Die update()-Methode aktualisiert den Zustand des Samens basierend auf den Kräften, die auf ihn wirken und der Physiksimulation.

    Mit this.vel.add(this.acc); wird die Geschwindigkeit (vel) durch die Beschleunigung (acc) aktualisiert und dann mit this.pos.add(this.vel); zur Position (pos) addiert. Dies entspricht dem physikalischen Prinzip der Beschleunigung.

    Die Beschleunigung wird mit this.acc.mult(0); nach jedem erneuten klicken auf »Play« im Editor zurückgesetzt, also auf Null gesetzt, um sicherzustellen, dass keine dauerhafte Beschleunigung auf den Samen wirkt und somit der Samen nicht unkontrolliert beschleunigt wird.

    In if (this.fly) { this.ori = lerp(this.ori, this.dir, 0.1); } wird überprüft, ob der Samen fliegt. Wenn er fliegt, also fly ist true, wird seine Ausrichtung nach und nach in Richtung der aktuellen Flugrichtung (dir) geändert.

    this.ori = (this.ori < PI / 2) ? this.ori + PI * 2 : this.ori; stellt sicher, dass die Orientierung des Samens im Bereich von 0 bis 2π bleibt, um eine kontinuierliche Rotation zu gewährleisten. Wenn die Orientierung kleiner als π/2 ist, wird sie um 2π erhöht, um sicherzustellen, dass sie im richtigen Bereich bleibt. In der darauffolgenden Zeile passiert ähnliches, nur dass der Wert nicht um 2π erhöht, sondern verringert wird.

    In this.dir = 3 * PI / 2 + this.random; wird die Zielorientierung des Samens neu festgelegt, basierend auf einem Ausgangswert von 3*π/2 (entsprechend einer vertikalen Ausrichtung nach oben) und einem zufälligen Offset (this.random). Dies bestimmt, in welche Richtung sich der Samen bewegen soll.

    Kraft anwenden

    applyForce(f) {
        if (f.mag() > panel.strength) {
          this.fly = true;
          let force = f.copy();
          force.div(this.mass);
          this.acc.add(force);
          this.dir = f.heading() + PI * 2 + this.random;
        }

    Die applyForce(f)-Methode wendet eine Kraft (f) auf den Samen an (Instanzen der Seed-Klasse), um seine Bewegung beeinflussen zu können. Diese Methode steuert also, wie sich die Samenobjekte unter dem Einfluss äußerer Kräfte verhalten und wie sie sich im Raum bewegen.

    if (f.mag() > panel.strength) { ... } überprüft, ob die Stärke der angewendeten Kraft größer ist als die in der vom Benutzer eingestellten Stärke (panel.strength) in der GUI. Wenn der Flugstatus des Samens auf true gesetzt wird, bedeutet dies, dass die Kraft stark genug ist, um den Samen fliegen zu lassen. Die angewendete Kraft wird entsprechend der Masse des Samens aufgeteilt, um eine realistische Beschleunigung zu erzielen.

    this.fly = true; setzt das fly-Attribut des Samens auf true, um anzuzeigen, dass der Samen fliegt.

    let force = f.copy(); kopiert die übergebene Kraft f in eine neue Variable namens force.

    force.div(this.mass); teilt die Kraft durch die Masse des Samens, um die Beschleunigung zu berechnen.

    this.acc.add(force); addiert die berechnete Beschleunigung zur aktuellen Beschleunigung des Samens.

    this.dir = f.heading() + PI * 2 + this.random; setzt die Flugrichtung des Samens auf den Winkel der übergebenen Kraft plus einen zufälligen Wert.

    if (f.mag() > 0.2) {
          this.dir = lerp(this.ori, f.heading() + PI * 2, 0.5);
          this.ori = lerp(this.ori, this.dir, 0.1);
        } else {
          this.ori = lerp(this.ori, this.oriOri, 0.05);
        }
      }

    if (f.mag() > 0.2) { ... } else { ... }: Wenn die übergebene Kraft f kleiner oder gleich 0.2 ist, wird der Samen nicht fliegen gelassen.

    Wenn die Kraft groß genug ist (f.mag() > 0.2), wird der Samen in Richtung der Kraft ausgerichtet und seine Orientierung schrittweise geändert.

    this.dir = lerp(this.ori, f.heading() + PI * 2, 0.5); ermittelt die aktuelle Orientierung des Samens in Richtung der Kraftrichtung.

    this.ori = lerp(this.ori, this.dir, 0.1); ermittelt die aktuelle Orientierung des Samens in Richtung der neuen berechneten Richtung.

    Wenn die Kraft nicht groß genug ist (f.mag() <= 0.2), wird die Orientierung des Samens langsam auf seine ursprüngliche Orientierung zurückgeführt.

    this.ori = lerp(this.ori, this.oriOri, 0.05); ermittelt die aktuelle Orientierung des Samens in Richtung seiner ursprünglichen Orientierung mit einer geringeren Geschwindigkeit.

    Zeichnung des Samens

    display() {
        push();
        translate(this.pos.x, this.pos.y);
        push();
        rotate(this.ori + 0.05 * sin(this.random + frameCount * 0.1));
        
        colorMode(HSB);
        fill(this.hue, 100, 100, 100);
    
        noStroke();
        ellipse(this.size * 2, 0, this.size);
    
        pop();
        pop();
      }
    }

    Die display()-Methode ist dazu da, um den Samen auf dem Bildschirm zu zeichnen.

    push() speichert den aktuellen Zeichenstatus, sodass nachfolgende Transformationen und Zeichenoperationen isoliert sind und den ursprünglichen Status nicht beeinflussen.

    translate(this.pos.x, this.pos.y) verschiebt den Ursprung des Koordinatensystems an die Position des Samensobjekts (this.pos.x, this.pos.y). Dadurch wird der Samen an die richtige Position auf dem Bildschirm verschoben.

    Der aktuelle Zeichenstatus wird mit push() erneut gespeichert. Dies ist nützlich, um die Transformationen und Zeichenoperationen innerhalb des nachfolgenden pop()-Aufrufs zu isolieren.

    rotate(this.ori + 0.05 sin(this.random + frameCount 0.1)) rotiert den Samen um seinen eigenen Ursprung (translate()-Position) basierend auf der aktuellen Orientierung (this.ori) und einer dynamischen zusätzlichen Rotation, die sich mit der Zeit ändert (sin(this.random + frameCount * 0.1)). Wodurch man einen schwankenden, organischen Bewegungseffekt erhält.

    colorMode(HSB) setzt den Farbmodus auf HSB (Farbton, Sättigung, Helligkeit), was es ermöglicht, die Farbe des Samens basierend auf einem Farbtonwert (this.hue) festzulegen.

    Mit fill(this.hue, 100, 100, 100) wird die Füllfarbe für den Samen auf eine Farbe im HSB-Farbmodus gesetzt. this.hue bestimmt den Farbtonwert, während die anderen Werte für die Sättigung (100), Helligkeit (100) und den Alpha-Kanal (100) festgelegt sind.

    Durch noStroke() wird erzielt, dass der Samen keine Umrandung erhält.

    ellipse(this.size * 2, 0, this.size) zeichnet eine Ellipse, also einen Kreis, am Ursprung des Koordinatensystems (0, 0) des Samensobjekts. Die X-Koordinate (this.size * 2) bestimmt den Abstand der Ellipse vom Ursprung in horizontaler Richtung, während die Y-Koordinate (0) den Abstand in vertikaler Richtung angibt. Die Größe der Ellipse wird durch this.size festgelegt.

    pop() stellt den Zeichenstatus vor dem letzten push()-Aufruf wieder her, was bedeutet, dass alle Transformationen und Zeichenoperationen, die nach dem letzten push() erfolgt sind, nun nicht mehr gelten.

    pop() stellt den ursprünglichen Zeichenstatus wieder her, der vor dem ersten push()-Aufruf gespeichert wurde. Alle Transformationen und Zeichenoperationen, die nach dem ersten push() erfolgt sind, werden jetzt rückgängig gemacht.

    Vollständiger Code

    "use strict";
    let mic, gui, micLevel;
    let controller, panel;
    let dandelions = [];
     
    function setup() {
      createCanvas(windowWidth, windowHeight);
      dandelions.push(new Dandelion(width / 2, height / 2));
     
      // setup GUI
      controller = function() {
        this.debug = false;
        this.strength = 0.2;
        this.number = 3;
        this.day = true;
      }
      panel = new controller();
      gui = new dat.GUI();
      gui.add(panel, 'debug');
      gui.add(panel, 'strength', 0.2, 0.8);
      gui.add(panel, 'number', 1, 7).step(1);
      gui.add(panel, 'day');
     
      // setup mic
      mic = new p5.AudioIn()
      mic.start();
    }
     
    function draw() {
      background((panel.day)? 255 : 50);
     
      // get mic
      micLevel = mic.getLevel();
      if (panel.debug) {
        ellipse(width * 0.1, constrain(height - micLevel * height * 5, 0, height), 10, 10);
      }
     
      for (let i = 0; i < dandelions.length; i++) {
        let d = dandelions[i];
        d.applyForce(createVector(micLevel, random(-0.12,0.1)));
        d.display();
      }
     
     
      if (dandelions.length > panel.number) {
        dandelions.shift();
      }
     
    }
     
    function doubleClicked() {
      dandelions.push(new Dandelion(mouseX, mouseY));
    }
     
     
    class Dandelion {
      constructor(x, y) {
        this.pos = createVector(x, y);
        this.rad = random(10, 20);
        this.seeds = [];
        this.seedN = this.rad + 8;
        for (let i = 0; i < this.seedN; i++) {
          let ori = i * 2 * PI / this.seedN + PI / 2;
          let vec = p5.Vector.fromAngle(ori);
          vec.mult(this.rad);
          let hue = random(360, 100); 
          this.seeds.push(new Seed(vec.x, vec.y, ori, this.seedN, hue)); 
        }
      }
     
      applyForce(f) {
        for (let i = 0; i < this.seedN; i++) {
          this.seeds[i].applyForce(createVector(f.x * random(1), f.y + random(-0.1, 0.1)));
          this.seeds[i].update();
        }
      }
     
      display() {
        push();
        translate(this.pos.x, this.pos.y);
        if (panel.day){
        fill(0, 0, 0, 200);
          stroke(0, 0, 0, 100);}
        else {
        fill(255);
        stroke(255);
        }
        ellipse(0,0,this.rad*2);
        strokeWeight(5);
        line(0, this.rad, 0, height);
        for (let i = 0; i < this.seedN; i++) {
          this.seeds[i].display();
        }
        pop();
      }
    }
     
    class Seed {
      constructor(x, y, ori, size, hue) {
        this.pos = createVector(x, y);
        this.vel = createVector(); 
        this.acc = createVector();
        this.ori = ori;
        this.oriOri = ori;
        this.size = size;
        this.mass = this.size / 4;
        this.fly = false;
        this.random = random(2);
        this.dir = 3 * PI / 2;
        this.hue = hue;
      }
     
      update() {
        this.vel.add(this.acc);
        this.pos.add(this.vel);
        this.acc.mult(0);
        if (this.fly) {
          this.ori = lerp(this.ori, this.dir, 0.1);
        }
        this.ori = (this.ori < PI / 2) ? this.ori + PI * 2 : this.ori;
        this.ori = (this.ori > PI * 5 / 2) ? this.ori - PI * 2 : this.ori;
        this.dir = 3 * PI / 2 + this.random;
      }
     
      applyForce(f) {
        if (f.mag() > panel.strength) {
          this.fly = true;
          let force = f.copy();
          force.div(this.mass);
          this.acc.add(force);
          this.dir = f.heading() + PI * 2 + this.random;
        }
        if (f.mag() > 0.2) {
          this.dir = lerp(this.ori, f.heading() + PI * 2, 0.5);
          this.ori = lerp(this.ori, this.dir, 0.1);
        } else {
          this.ori = lerp(this.ori, this.oriOri, 0.05);
        }
      }
     
      display() {
        push();
        translate(this.pos.x, this.pos.y);
        push();
        rotate(this.ori + 0.05 * sin(this.random + frameCount * 0.1));
        colorMode(HSB);
        fill(this.hue, 100, 100, 100);
     
        noStroke();
        ellipse(this.size * 2, 0, this.size);
     
        pop();
        pop();
      }
    }

    So sollte es nun aussehen

      Quelle

      Hier findest du die kompletten Code-Dateien zum herunterladen:

      Code-Dateien (ZIP)

      Wenn du sehen möchtest, welche Ursprungsdatei uns zu diesem Tutorial inspiriert hat, kannst Du sie dir gerne unter folgendem Link anschauen: https://wp.nyu.edu/shanghai-ima-documentation/electives/xx555/noc-week-7-midterm-dandelions-susan-xu/

      Wir wünschen Dir viel Spaß beim ausprobieren!


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