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) und8
(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) und4
(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.
7. Quellen
Die Umsetzung basiert auf folgenden Vorlagen und Inspirationsquellen:
Inspiration für das Tutorial: Instagram Reel von blankensmithing
Partikelsystem: Image to Point Cloud von 小钻风
Handtracking mit Mediapipe: Medium-Artikel von Andrés Villa Torres