Dynamic gesture particles

In diesem Tutorial lernst du, mithilfe von p5.js Bibliotheken, deine eigene interaktive und bunte Partikelwelt zu entwickeln, bei der du selber bestimmen kannst, welche Farben, Partikelgröße- und Geschwindigkeit deine Walker haben sollen. Mit Hand- und Eyetracking bewegst du sie selbst und kannst diese auch zum Explodieren bringen!

Die Idee

Was sind Dynamic gesture Particles?

Die Idee basiert auf einem generativen, abstrakten Partikelsystem mit regelbarer Anzahl an Partikeln, sowie Größe, Geschwindigkeit und Farbe. Durch Gestensteuerung oder Eyetracking kann die Bewegung der einzelnen Partikel beeinflusst werden oder Effekte wie Explosionen ausgelöst werden. So kann beispielsweise durch ein Blinzeln beim Eyetracking eine Explosion erzeugt werden.


In diesem Projekt sollte die Steuerung bewusst ohne klassische Eingabegeräte erfolgen. Stattdessen setzten wir von Anfang an auf Handtracking und ergänzten später Eyetracking, um die Interaktion noch intuitiver zu gestalten.

Das Tutorial basiert auf einer Kombination aus verschiedenen Elementen, bei der die Interaktivität im Mittelpunkt stand:

  • Interaktivität (Gestensteuerung)
  • Benutzeroberfläche (UI)
  • Webcam-Intergration

Steuerung

  • Handgestensteuerung

    • Zeigefinger = Wird blau angezeigt; Partikelsteuerung

    • Faust = Lilane Farbe; löst Explosion aus

  • Eyetracking

    • Pupillenbewegung = Neon cyan; Partikelsteuerung

    • Blinzeln = Löst Explosion aus

  • Cursor Pointer

    • Klicken der Maus = Löst Explosion aus

    • Bedienung der Regler rechts; Farbauswahl

Explosion durch Faust | Handtracking

Ballt man die Hand zu einer Faust zusammen, färben sich die Punkte auf der linken oberen Webcam-Anzeige lila und die Partikel explodieren an einer Stelle.

Explosion durch Blinzeln | Eyetracking

Die Pupillen werden in der Webcam in einem leuchtenden Cyan-Neonfarbton dargestellt. Passend zur Blickrichtung erscheint ebenfalls ein leuchtender, cyanfarbener Punkt. Blinzelt man, wird wie im vorherigen Beispiel ein Effekt ausgelöst.


Wie setzen wir das um?

Im Folgenden lernst du, wie du das Partikelsystem technisch umsetzt – von HTML-Grundgerüst, über Hand- und Eyetracking bis zur Farbauswahl.


1. Überblick | Was brauchen wir?

  • HTML-Gerüst & Webcam einrichten
  • Partikelsystem mit PIXI.js
  • Handtracking mit MediaPipe
  • Eyetracking mit WebGazer
  • Interaktive Steuerung (Slider, Farben, Effekte)

2. Vorbereitung & Setup

Bevor wir mit dem eigentlichen Code starten, binden wir alle notwendigen Bibliotheken ein. Diese sorgen für Hand- und Eyetracking sowie für das visuelle Rendering unserer Partikel:

Eingesetzte Libraries:

  • Pixi.js: Partikelsystem / 2D-Rendering
  • Victor.js: Vektorberechnung (Bewegung)
  • MediaPipe Hands: Handtracking
  • WebGazer.js: Eyetracking per Webcam
<!-- MediaPipe Hands: Handtracking -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js"></script>

<!-- WebGazer: Eyetracking -->
<script src="https://webgazer.cs.brown.edu/webgazer.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh/face_mesh.js"></script>

<!-- Pixi.js: Grafik-Rendering & Victor.js: Vektormathematik -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pixi.js/7.2.4/pixi.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/victor/1.1.0/victor.min.js"></script>

3. UI-Elemente | Oberfläche

Für eine vollständige Benutzeroberfläche wurden UI-Elemente mit passenden Farbakzenten entwickelt. Diese umfassen:

3.1. Webcambild

Ein Webcam-Bild wurde integriert, damit der Benutzer die Handtracking- und Eyetracking-Daten in Echtzeit selbst überwachen und nachvollziehen kann. Das Webcambild wurde mit einem Overlay bewusst abgedunkelt, damit die Trackingpunkte noch mehr herausstechen können.

Html:

<video id="webcam" autoplay playsinline></video> <!-- Videoelement  für Webcam, öffnet automatisch und direkt-->
  <!-- Unser Punkt der zeigt wo wir hingucken -->
  <div id="gaze-pointer" style="display: none; position: absolute; width: 20px; height: 20px; background: #36f9c7; border-radius: 50%; transform: translate(-50%, -50%); box-shadow: 0 0 20px lime; z-index: 9999; pointer-events: none;"></div>
  <canvas id="overlay"></canvas>

Css:

#webcam, #dark-overlay, #overlay { /* positionierung von webcam und overlays */
  position: absolute;
  top: 10px;
  left: 10px;
  width: 200px;
  height: 150px;
  border-radius: 10px;

3.2. Partikel und Flowfield

Wir erzeugen etwa 30.000 Partikel, die sich im Flow bewegen, aufeinander reagieren und an den Bildschirmkanten abprallen.

class Walker {
  constructor(texture, size, radius) {
    this.vector = new Victor(Math.random() * 2 - 1, Math.random() * 2 - 1);
    this.sprite = new PIXI.Sprite(texture);
    this.sprite.anchor.set(0.5);
    this.sprite.scale.set(0);
  }
}

const PARTICLE_COUNT = 30000; //Anzahl der Partikel
const GRID_RESOLUTION = 30; //Rasterweite für Partikelbewegung

Ein Flowfield ist ein Gitter aus Vektoren, das eine simulierte Strömung repräsentiert. Diese Strömung wird in jedem Frame neu berechnet damit sich unsere Partikelchen, oder auch unsere «Walker», natürlich bewegen können.

move(gridVector, delta) { //berechnet jede bewegung pro frame neu
    this.vectorWeight += 0.05 * delta;
    if(this.vectorWeight > 1) this.vectorWeight = 1;
    this.speed -= 0.06 * delta * window.app.speedModifier;
    if(this.speed <= 0.06) this.speed = 0
    
    this.vector.mix(gridVector, 0.7 * this.vectorWeight).normalize()
    
    let newX = this.sprite.x + this.vector.x * this.speed;
    let newY = this.sprite.y + this.vector.y * this.speed;
    
    const TL = new Victor(this.radius, this.radius);
    const BR = new Victor(
      this.size.x * (this.size.grid + this.size.gutter) - this.radius,
      this.size.y * (this.size.grid + this.size.gutter) - this.radius
    )
    
    if(newX < TL.x || newX > BR.x ) this.vector.invertX();
    if(newY < TL.y || newY > BR.y ) this.vector.invertY();
    
    if(newX < TL.x) this.sprite.x = TL.x;
    if(newX > BR.x) this.sprite.x = BR.x;
    
    if(newY < TL.y) this.sprite.y = TL.y;
    if(newY > BR.y) this.sprite.y = BR.y;
    
    this.sprite.x += (this.vector.x * delta) * this.speed;
    this.sprite.y += (this.vector.y * delta) * this.speed;
    
    if(this.speed > 4 && Math.random() > 0.99) this.sparkleCount = 1;
    this.sparkleCount -= 0.1;
    if (this.sparkleCount < 0) this.sparkleCount = 0;

3.3. Eyetracking-Toggle

Der Toggle wurde passend zur Pupillenfarbe im Eyetracking-Modus und zur Farbe der Regler gestaltet. Beim Aktivieren leuchtet er auf und ist unten links auf der Benutzeroberfläche positioniert.

<div class="toggle-wrap">
  <input class="toggle-input" id="toggle" type="checkbox" />
  <label class="toggle" for="toggle">
    <div class="track">
      <div class="bg-layer"></div>
      <div class="grid-layer"></div>
      <div class="highlight"></div>
    </div>
    <div class="thumb">
      <div class="ring"></div>
      <div class="core">
        <div class="icon">
          <div class="wave"></div>
          <div class="pulse"></div>
        </div>
      </div>
    </div>
    <div class="gesture"></div>
    <div class="feedback">
      <div class="ripple"></div>
      <div class="progress"></div>
    </div>
    <div class="status">
      <div class="indicator">
        <div class="dot"></div>
        <div class="text"></div>
      </div>
    </div>
  </label>
</div>
Get Toggle

3.4. Handtracking mit MediaPipe Hands

MediaPipe Hands ist eine von Google entwickelte Lösung für präzises, kamerabasiertes Handtracking in Echtzeit. Die Technologie verwendet neuronale Netze, um Hände im Videobild zu erkennen und insgesamt 21 Schlüsselstellen auf der Hand zu bestimmen.

Erst wird die Hand in der Kameraansicht lokalisiert, dann werden für die gefundene Region die Koordinaten geschätzt. Diese Punkte können genutzt werden, um Gesten, Bewegungen oder bestimmte Handhaltungen (bei uns wichtig: die Faust) zu erkennen.

Wir benutzen also MediaPipe Hands, um Fingerpositionen live auf ein Canvas zu zeichnen. Wenn die Faust erkannt wird, entsteht ein spezieller Partikeleffekt ­– eine Explosion.

const hands = new Hands({
  locateFile: file => `https://cdn.jsdelivr.net/npm/@mediapipe/hands/${file}`
});

hands.setOptions({
  maxNumHands: 1,
  modelComplexity: 0,
  minDetectionConfidence: 0.7,
  minTrackingConfidence: 0.7
});
hands.onResults(results => {
  // Canvas pro Frame löschen
  resizeCanvas();
  ctx.clearRect(0, 0, canvas.width, canvas.height);

  if (results.multiHandLandmarks && results.multiHandLandmarks.length > 0) {
    const landmarks = results.multiHandLandmarks[0];
};
  • Wenn eine Hand erkannt wird, zeichnen wir sie.
  • Bestimmte Bewegungen (z.B. die Faust) lösen Partikeleffekte aus.
Wichtig für das Zeichnen
const connections = [
      [0, 1], [1, 2], [2, 3], [3, 4],
      [5, 6], [6, 7], [7, 8],
      [9,10], [10,11], [11,12],
      [13,14], [14,15], [15,16],
      [17,18], [18,19], [19,20],
      [0, 5], [5, 9], [9, 13], [13, 17], [17, 0]
    ];

    ctx.strokeStyle = '#00ff00';
    ctx.lineWidth = 2;
    connections.forEach(([a, b]) => {
      const p1 = landmarks[a];
      const p2 = landmarks[b];
      ctx.beginPath();
      ctx.moveTo(p1.x * canvas.width, p1.y * canvas.height);
      ctx.lineTo(p2.x * canvas.width, p2.y * canvas.height);
      ctx.stroke();
    });

Faust-Erkennung

const fingertips = [8, 12, 16, 20].map(i => landmarks[i]);

const avg = fingertips.reduce((acc, p) => {
  acc.x += p.x;
  acc.y += p.y;
  return acc;
}, { x: 0, y: 0 });

avg.x /= 4;
avg.y /= 4;

const spread = fingertips.reduce((acc, p) => {
  return acc + Math.hypot(p.x - avg.x, p.y - avg.y);
}, 0) / 4;

const isFist = spread < 0.04;
const indexFingerTip = landmarks[8];
const wrist = landmarks[0];

3.5. Eyetracking mit Webgazer

WebGazer ist eine JavaScript-basierte Bibliothek, die Eye-Tracking ohne zusätzliche Hardware ermöglicht. WebGazer nutzt ganz einfach die normale Webcam des Nutzers. Dabei werden Gesichts- und Augenbereiche erkannt, und aus den Bewegungen der Pupillen wird abgeschätzt, wohin der Nutzer auf dem Bildschirm blickt.

Die Positionsschätzung basiert auf maschinellem Lernen und Regressionsmodellen (z.B. Ridge Regression). WebGazer eignet sich gut für einfache interaktive Anwendungen oder Demos, erreicht aber noch nicht die Präzision professioneller Eye-Tracking-Systeme, für unser Projekt reicht das aber erstmal aus.

let webgazerRunning = false;
let faceMesh;
let cameraGaze;
const gazePointer = document.getElementById('gaze-pointer');
function startEyeTracking() {
  if (webgazerRunning) return;

  webgazerRunning = true;
  gazePointer.style.display = 'block';

  webgazer.setGazeListener((data, elapsedTime) => {
    if (data == null) return;

    // Pointer bewegen
    gazePointer.style.left = `${data.x}px`;
    gazePointer.style.top = `${data.y}px`;

    // Partikel erzeugen
    window.app.cursorPoints.push({
      position: new Victor(data.x, data.y),
      direction: new Victor(0, 0),
      distance: 10,
      explode: false
    });
  });

Explosion durch Blinzeln

Im startEyeTracking()-Block wird MediaPipe FaceMesh genutzt, um Augenlandmarken zu erkennen, insbesondere:

const leftEyeTop = landmarks[159];
const leftEyeBottom = landmarks[145];
const rightEyeTop = landmarks[386];
const rightEyeBottom = landmarks[374];

Mit diesen Punkten wird die vertikale Höhe beider Augen berechnet:

const leftEyeDist = Math.hypot(
  (leftEyeTop.x - leftEyeBottom.x),
  (leftEyeTop.y - leftEyeBottom.y)
);

const rightEyeDist = Math.hypot(
  (rightEyeTop.x - rightEyeBottom.x),
  (rightEyeTop.y - rightEyeBottom.y)
);

Dann prüft man mit: const isBlink = (leftEyeDist + rightEyeDist) / 2 < 0.015;

Wenn die durchschnittliche Augenöffnung unter 0.015 fällt , dann wird davon ausgegangen, dass man blinzelt und es geschieht eine Explosion:

Wenn isBlink === true:

for (let i = 0; i < 15; i++) {
  window.app.cursorPoints.push({
    position: new Victor(explosionX, explosionY),
    direction: new Victor(Math.random() * 2 - 1, Math.random() * 2 - 1),
    distance: 50,
    explode: true
  });
}

Es werden 15 Partikel-Explosionen an der Position des gazePointer erzeugt, also dort, wo man hinschaut.

Darstellung der Gesichtslandmarks

Um den Erkennungsprozess (z. B. für Blinzeln) besser nachvollziehbar und ästhetischer zu gestalten, wird im Eye-Tracking-Modus ein blaues Gesicht im Overlay dargestellt.

Die MediaPipe FaceMesh-API erkennt dabei Landmark-Punkte pro Gesicht.

Das blaue Gesicht entsteht im Code durch folgende Befehle in startEyeTracking():

drawConnectors(faceCtx, landmarks, FACEMESH_TESSELATION, { color: '#36f9c7', lineWidth: 0.5 });
drawLandmarks(faceCtx, landmarks, { color: 'blue', radius: 0.3 });
  • drawConnectors(...): Verbindet bestimmte Landmark-Punkte mit Linien (helles Cyan).
  • drawLandmarks(...): Zeichnet kleine, blaue Punkte auf alle 468 Gesichtslandmarks.

4. Container

Über HTML-Elemente wie Slider und Dropdowns können Nutzer die Eigenschaften der Partikel selbst verändern

  • Geschwindigkeit: Wie schnell die Partikel von der Oberfläche verschwinden
  • Partikelgröße: Radius der Partikel 1-20px
  • Partikelanzahl: 1000-50000 Partikel
  • Farbschema: Auswahl einer beliebigen Farbe

Partikelgröße

Durch den Slider Partikelgröße kann der Benutzer den Radius (1-20px) der Partikel regulieren.

Kleiner Radius:

Großer Radius:

Der Codeabschnitt, der die Partikelgröße bestimmt, befindet sich JavaScript-Teil der Klasse App und der Funktion createWalkers()

const radius = this.particleRadius;
---------------------------------------------------
Zugehörige Zuweisung: 

sizeRange.addEventListener("input", () => {
  this.particleRadius = parseInt(sizeRange.value);
  this.createWalkers();
});
  • Der Slider mit der ID sizeRange regelt die Partikelgröße.

  • Der gewählte Wert wird der Variable particleRadius zugewiesen

  • Dann wird createWalkers() aufgerufen, um die Partikel mit der neuen Größe neu zu generieren

In createWalkers() wird radius dann genutzt, um die Grafikgröße der Partikel zu zeichnen mit circleGraphic.drawCircle

Partikelanzahl

Mit dem Slider Anzahl Partikel kann der Nutzer zwischen 1.000 und 50.000 Partikeln wählen. Im gezeigten Beispiel ist der Regler auf das Minimum von 1.000 Partikeln eingestellt.

<label for="countRange">Anzahl Partikel</label><br>
<div class="slider-container">
  <input type="range" id="countRange" min="1000" max="50000" step="1000" value="30000" class="range">
  <div class="thumb-value" id="thumbValueCount">30000</div>
</div>

Im JavaScript-Teil bestimmt window.app.walkerTotal , wie viele Partikel erstellt werden. createWalkers() rendert sie neu.

const countRange = document.getElementById("countRange");

countRange.addEventListener("input", () => {
  window.app.walkerTotal = parseInt(countRange.value);  // Anzahl Partikel wird neu gesetzt
  window.app.createWalkers();  // Neue Partikel erzeugen mit aktualisierter Anzahl
});

4.1. Range Slider

Die Range Slider sind rechts oben in einem Container platziert. Diese spielen eine große Rolle bei der interaktiven Steuerung der Partikel, sei es zur Steuerung der Anzahl, der Größe oder der Geschwindigkeit. Verschiebt man den Regler so werden in der Mitte die genaue Anzahl der Partikel angezeigt.

<label for="Range">BeispielSlider</label><br> 
  <div class="slider-container">
    <input type="range" id="Range" min="0.1" max="3" step="0.1" value="1" class="range">
    <div class="thumb-value" id="thumbValueSpeed">1</div>
  </div><br><br>
Get Slider

4.2. Interaktive Farbpalette

Die interaktive Farbpalette ist im Container mit den Slidern integriert. Über sie kann der Nutzer flexibel die Farbkombination der Partikel steuern und entscheiden, ob diese einfarbig oder mehrfarbig dargestellt werden sollen.

Html:

<div class="header-row"> <!-- Überschrift für Farbauswahl -->
    <span class="header-label">Farben auswählen</span>
  </div>
  
  <div class="slider-wrapper"> <!-- Slider für Farbauswahl mit Color picker und checkbox -->
    <div class="slider-container">
      <input type="range" min="1" max="5" step="1" value="2" class="range" id="sliderColors">
      <div class="thumb-value" id="thumbValueColors">2</div>
    </div>
  
    <div id="colorPickers" class="color-picker-container"></div>
  
    <div class="checkbox-row">
      <label class="checkbox-container">
        <input type="checkbox" id="buntCheckbox" checked>
        <span class="checkbox-label">Bunt</span>
      </label>
    </div>
  </div>

Javascript:

function getCurrentParticleColor() { //Entscheidet on regenbogen oder selber entschiedene farben
  if (buntCheckbox.checked) {
    const hue = (window.app.hueCount % 360);
    return hslToHex(hue, 100, 50);
  } else {
    const randomColor = customColors[Math.floor(Math.random() * customColors.length)];
    return randomColor;
  }
}

function updateColorPickers() { //damit farbe auswählen bereich funktioniert aktualisieren wir die color picker jedesmal
  colorPickersContainer.innerHTML = '';

  const count = parseInt(colorSlider.value);
  thumbValueColors.textContent = count;
  thumbValueColors.style.display = 'block';

  const oldColors = [...customColors];
  customColors = [];

  for (let i = 0; i < count; i++) {
    if (oldColors[i]) {
      customColors.push(oldColors[i]);
    } else {
      customColors.push(getRandomColor());
    }
  }

Farbschema

Das Ziel war Cyan neonleuchtende kleine Akzente reinzusetzen mit der Hexfarbe #36f9c7. Diese wurde in nahezu allen Elementen benutzt, um ein kohärentes Erscheinungsbild zu gewährleisten.


Steuerbare Partikelfarben

Partikelfarben steuerbar per Startoption, Checkbox und Farbschieberegler im Container

  • User kann mit einem Slider bestimmen, wie viele Farben es gibt
  • Standardmäßig ist eine "Bunt"-Checkbox aktiviert (= zufällige Regenbogenfarben)
  • Wenn der User eine Farbe auswählt, soll die Checkbox automatisch deaktiviert werden (= eigene Farbauswahl aktiv)
  • Wenn Checkbox aktiv → werden Partikel bunt, ansonsten nach den gewählten Farben.

Startfarbe der Partikel

Im Anfangszustand sind die Partikel bunt, die Farben gehen weich ineinander über und erzeugen eine fließende Mischung. Standardmäßig ist der Checkbutton bei "Bunt" aktiviert.

Auswählbare Farbe durch Benutzer

Wird die Checkbox deaktiviert, ein Regler bewegt oder ein Farbkreis angeklickt, wird die bunte Standardfarbe deaktiviert. Der Nutzer kann anschließend eine individuelle Farbe auswählen: Entweder eine beliebige Farbe aus der Farbpalette oder eine Kombination aus 1 bis 5 frei wählbaren Farben. Die Anzahl der verwendeten Farben kann dabei über den separaten Regler "Farben auswählen" bestimmt werden.

Nachdem der Nutzer seine Farben ausgewählt hat, werden diese individuell in einem „Konfetti-Effekt“ dargestellt.

Customize Colors

Fazit

Durch dieses Projekt haben wir nicht nur den Umgang mit Hand-, Gesichts- und Augentracking gelernt, sondern auch ein besseres Verständnis für abstrakte, interaktive Programmierung entwickelt. Besonders spannend war es, eine intuitive Benutzeroberfläche zu gestalten, bei der Nutzer selbst entscheiden können, wie ihre Partikel aussehen in Farbe, Größe oder Verhalten.


Ergebnis

Tutorial-Dateien herunterladen


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