Dynamische Galerie in p5.js

In diesem Projekt haben wir ein interaktives System entwickelt, das die Navigation durch eine Bildergalerie allein über Handgesten ermöglicht.

1. Ziel des Projekts

Das Ziel war es, eine interaktive Galerie zu gestalten, die:

  • auf natürliche Handbewegungen reagiert,

  • sanfte, visuell ansprechende Übergänge zwischen Bildern erzeugt,

  • Rotation und Zoom über Gesten ermöglicht,

  • eine räumliche Tiefe schafft.

Besonderes Augenmerk lag auf der Kombination von Gestensteuerung, Partikeldarstellung und flüssiger visueller Übergänge.


2. Bilder vorbereiten und laden

2.1 Bilder hochladen

Damit eigene Bilder genutzt werden können, müssen diese zunächst im p5.js Web Editor hochgeladen werden:

  • Im Editor auf das Plus neben „Sketch-Files“ klicken.
  • Über „Upload File“ die gewünschten Bilder hinzufügen.
  • Empfehlenswert sind Bilder mit ähnlichen Abmessungen und Farbstimmungen, um ein harmonisches Wechselverhalten zu erreichen.

Dateiformate wie .jpg oder .png sind ideal.

2.2 Bilder preloaden und in ein Array speichern

Damit die Bilder beim Start des Sketches sofort verfügbar sind, werden sie im preload()-Block geladen und in einem Array gespeichert:

let images = [];
let currentImageIndex = 0;
let img;

function preload() {
  images.push(loadImage("Strand.jpg"));
  images.push(loadImage("Kaktus.jpg"));
  images.push(loadImage("Ziege.jpg"));
  images.push(loadImage("Orange.jpg"));
  // Weitere Bilder können ergänzt werden

  img = images[currentImageIndex];
}

Das Array dient dazu, die geladenen Bilder in einer festen Reihenfolge zu speichern, sodass beim Erkennen einer Bildwechsel-Geste immer klar ist, welches Bild aktuell angezeigt wird und welches als Nächstes geladen werden soll.


3. Handtracking

3.1 Bibliotheken einbinden

Für die Erkennung der Handgesten verwenden wir die Mediapipe Hands. Die Einbindung erfolgt über Skript-Tags im HTML-Bereich:

<!-- MediaPipe Kamera-Utilities (für Zugriff auf Webcam und Frames) -->
<script src="https://cdn.jsdelivr.net/npm/@mediapipe/camera_utils/camera_utils.js" crossorigin="anonymous"></script>

<!-- MediaPipe Hands - entweder via CDN oder lokal -->
<!-- CDN-Version (auskommentiert – nicht aktiv) -->
<!-- <script src="https://cdn.jsdelivr.net/npm/@mediapipe/hands/hands.js" crossorigin="anonymous"></script> -->

<!-- Lokale Version (aktiv) -->
<script src="hands.js"></script>

Innerhalb des js-Datei wird dann ein Handtracking-Objekt erzeugt, das kontinuierlich die Landmark-Koordinaten der Hände ausliest.


4. Partikelsystem

Der Kern der visuellen Darstellung ist ein Partikelsystem, bei dem jedes Bild in eine Vielzahl von Partikeln zerlegt wird:

  • Jeder Partikel entspricht einem Pixel des Originalbildes.

  • Die Partikel speichern ihre Position und Farbe.

  • Über Handbewegungen werden sie im Raum verschoben, skaliert oder rotiert.

Die Partikel werden dynamisch aktualisiert, wodurch der Eindruck entsteht, dass sich die Bilder organisch auflösen und neu zusammensetzen.

function setup() {
  img.loadPixels();
  createPointCloud();
}

function draw() {
  push();
  scale(zoom);
  rotateY(rotationY);
  translate(camOffsetX, camOffsetY, 0);

  // Zeichnet einzelne Partikel
  for (let i = 0; i < vertices.length; i++) {
    let v = vertices[i];
    let c = colors[i];
    stroke(c[0], c[1], c[2]);
    strokeWeight(1.5);
    point(v[0], v[1], v[2]);
  }

  pop();

  // Sanfter Übergang
  zoom = lerp(zoom, targetZoom, 0.1);
  rotationY = lerp(rotationY, targetRotationY, 0.1);
}


// Umwandlung Bild → Partike
function createPointCloud() {
  vertices = [];
  colors = [];

  const xAdd = -img.width / 2;
  const yAdd = -img.height / 2;

  for (let y = 0; y < img.height; y += 4) {
    for (let x = 0; x < img.width; x += 4) {
      const index = 4 * (y * img.width + x);
      const r = img.pixels[index];
      const g = img.pixels[index + 1];
      const b = img.pixels[index + 2];

      const z = random(-100, 100); // Tiefe für 3D-Effekt

      vertices.push([x + xAdd, y + yAdd, z]);
      colors.push([r, g, b]);
    }
  }
}

5. Gestensteuerung

Die Galerie wird komplett über Handgesten gesteuert. Hier erklären wir die wichtigsten Mechaniken mit den zugehörigen Codeteilen aus unserem Projekt:

5.1 Tiefenwirkung und Zoom – Steuerung durch Abstand zwischen Daumen und Zeigefinger

// Tiefenwirkung steuern mit rechter Hand
  if (right) {
    const d = dist(right[4].x, right[4].y, right[8].x, right[8].y);
    distortion = map(d, 0.05, 0.2, 0, 4);
    distortion = constrain(distortion, 0, 4);
  }
}

Was passiert hier?

  • Die Landmark-Punkte 4 (Daumenspitze) und 8 (Zeigefingerspitze) werden ausgelesen.

  • dist() berechnet die Distanz zwischen diesen beiden Punkten im Raum.

  • Je näher die Finger zusammen sind, desto kleiner wird der Wert.

Anschließend wird der Abstand bei distortion umgerechnet.

  • Bei sehr kleinem Abstand (0.05) keine Tiefe (0).

  • Bei großem Abstand (0.2) starke Tiefe (4).

Die so berechnete Verzerrung (distortion) verändert im Rendering der Punktwolke den Z-Wert der Partikel, wodurch der Eindruck von Tiefe entsteht.

Der Code für den Zoom folgt der selben Logik.

5.2 Rotation – Steuerung durch Bewegung des Zeigefingers

// Rotation mit rechter Hand (horizontale Bewegung des Zeigefingers)
  if (right) {
    const fingerTip = right[8]; // rechter Zeigefinger

    if (!fingerTip) return;

    const x = fingerTip.x;
    if (lastRightX !== null) {
      const dx = x - lastRightX;
      targetRotationY -= dx * 5; // gleiche Multiplikation wie vorher
    }
    lastRightX = x;
  } else {
    lastRightX = null;
  }

Was passiert hier?

  • Landmark-Punkt 8 (Zeigefingerspitze) der rechten Hand wird ausgelesen.

  • Die x-Koordinate (horizontale Position) wird gespeichert.

  • Es wird der Unterschied zur vorherigen Position berechnet: dx.

  • Je nachdem, ob sich der Finger nach rechts oder links bewegt, ändert sich dx.

  • Der Wert dx wird mit 5 multipliziert, um kleine Bewegungen stärker zu übersetzen.

    → Positive Bewegung = Rotation nach links
    → Negative Bewegung = Rotation nach rechts

5.3 Bildwechsel – Steuerung durch Daumenposition

// Bildwechsel durch Daumenposition
if (right) {
  const wrist = right[0];      // Handgelenk
  const thumbTip = right[4];   // Daumenspitze

  if (!wrist || !thumbTip) return;

  const yDiff = thumbTip.y - wrist.y;
  const xDiff = abs(thumbTip.x - wrist.x);

  const thresholdUp = -0.04;     // Daumen deutlich über Handgelenk
  const xThreshold = 0.04;       // Fast senkrecht (kleine X-Abweichung)
  const thresholdDown = 0.04;    // Für vorheriges Bild

  if (imageSwitchCooldown === 0) {
    // NÄCHSTES Bild: Daumen über Handgelenk UND fast senkrecht
    if (yDiff < thresholdUp && xDiff < xThreshold) {
      currentImageIndex = (currentImageIndex + 1) % images.length;
      loadCurrentImage();
      imageSwitchCooldown = 15;
    } 
    // VORHERIGES Bild: Daumen unter Handgelenk 
    else if (yDiff > thresholdDown) {
      currentImageIndex = (currentImageIndex - 1 + images.length) % images.length;
      loadCurrentImage();
      imageSwitchCooldown = 15;
    }
  }
}

Was passiert hier?

  • Die Landmark-Punkte 0 (Handgelenk) und 4 (Daumenspitze) der rechten Hand werden ausgelesen.
  • yDiff misst den vertikalen Abstand des Daumens relativ zum Handgelenk.
  • xDiff prüft, ob der Daumen nahezu senkrecht nach oben oder unten steht (geringe horizontale Abweichung).

Drei Schwellenwerte steuern die Auslösung:

  • thresholdUp = -0.04 → Daumen deutlich über dem Handgelenk.

  • thresholdDown = 0.04 → Daumen deutlich unter dem Handgelenk.

  • xThreshold = 0.04 → Damit seitliche Bewegungen ignoriert werden.

Die Aktion wird nur ausgelöst, wenn eine Cooldown-Zeit (imageSwitchCooldown) abgelaufen ist – damit nicht mehrere Bilder gleichzeitig übersprungen werden.

Wenn der Daumen über dem Handgelenk ist und nahezu senkrecht steht, wird zum nächsten Bild gewechselt (currentImageIndex + 1).

Wenn der Daumen unter dem Handgelenk ist, wird ändert sich das Bild zum vorherigen (currentImageIndex - 1).

Nach jedem Wechsel wird imageSwitchCooldown auf 15 gesetzt, um erneute Auslösung kurzzeitig zu blockieren.

5.4 Bewegung – Steuerung durch Handbewegung

// Kamera-Offset durch linke Handbewegung
  if (left) {
    const currentPos = { x: left[0].x, y: left[0].y };

    if (lastLeftHandPos !== null) {
      const dx = currentPos.x - lastLeftHandPos.x;
      const dy = currentPos.y - lastLeftHandPos.y;

      camOffsetX -= dx * 750;
      camOffsetY += dy * 750;
    }

    lastLeftHandPos = currentPos;
  } else {
    lastLeftHandPos = null;
  }

Was passiert hier?

Als Landmark-Punkte wird das Linke Handgelenk verwendet 0.

Zunächst wird die aktuelle Position des Handgelenks gespeichert (currentPos). Dann wird geprüft, ob es eine vorherige Position (lastLeftHandPos) gibt. Wenn ja:

  • Wird der Unterschied in x-Richtung (dx) und y-Richtung (dy) berechnet.

  • Diese Differenzen beschreiben die Bewegung der Hand zwischen zwei Frames.

Im nächsten Schritt wird die Kamera verschoben (camOffsetX, camOffsetY).

  • Bewegung nach rechts → Kamera bewegt sich nach links (negatives dx).

  • Bewegung nach oben → Kamera bewegt sich nach oben (positives dy ).

Die Multiplikation mit 750 skaliert die kleinen Bewegungen der Hand zu einer sichtbaren Kamerabewegung auf dem Bildschirm.


9. Eingebundene Anleitungsgrafik

Zur besseren Übersicht über die wichtigsten Gesten und Funktionen haben wir zusätzlich eine Anleitungsgrafik erstellt und eingebunden. Die Grafik zeigt welche Gesten welche Aktionen auslösen.

Das Bild wurde im HTML eingebunden und mithilfe einer CSS-Klasse im Viewport positioniert und skaliert.

<main>
  <!-- ANLEITUNG als überlagerndes Bild -->
  <img class="instruction" src="Anleitung.png" alt="Anleitung">
</main>
.instruction {
  position: fixed;
  bottom: 1.25rem;
  left: 1.5rem;
  width: 250px;    /* maximale Breite */
  height: auto;    /* Höhe automatisch, damit Seitenverhältnis bleibt */
  z-index: 10;     /* ÜBER dem p5-Canvas */
  pointer-events: none;
}

6. Finale Version

Hinweis: In der finalen Version sind die Handmarkierungen nicht sichtbar – sie dienen lediglich zur Veranschaulichung der Gestensteuerung in diesem Tutorial.

Hier Code herunterladen

7. Quellen

Die Umsetzung basiert auf folgenden Vorlagen und Inspirationsquellen:

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.