IxD2 | Die Verfolgung
Dieses Projekt erstellt eine abstrakte Animation, welche mithilfe von Personentracking interaktiv gestaltet wird. Dabei folgen abstrahierte Formen den dynamischen Bewegungen der Personen, wobei ein unheimliches Gefühl geweckt werden soll.
Projekt Übersicht
Unser Semesterprojekt erforscht, wie Installationen, Projektionen, Terminals, Sensoren und Spiele Räume bereichern und erlebnisreicher gestalten können. Künstlerische und spielerische Ansätze sind dabei gewünscht.
Unser Idee
Wir entwickeln eine abstrakte Animation, die durch Personentracking interaktiv wird. Abstrahierte Formen folgen den Bewegungen der Personen im Raum, um ein unheimliches Gefühl zu erzeugen und die Wahrnehmung der Besucher zu intensivieren. Diese Technologie ermöglicht eine direkte, intuitive Interaktion, wodurch der Raum auf neue, ungewöhnliche Weise erlebt werden kann.
Wie setzten wir es um?
Unser Projekt nutzt die Bibliothek p5.js für die grafische Darstellung und ml5.js für das Personentracking. Eine Webcam erfasst die Bewegungen, ein Beamer projiziert die Animationen, und ChatGPT unterstützt uns bei der Codeentwicklung. So schaffen wir eine interaktive Erfahrung.
Finalisierung des Aussehens
Überarbeitung des Designs
Unser Ziel war es, das Auge weiter zu abstrahieren und transparenter zu machen. Dafür ließen wir uns von einem Moodboard inspirieren, das aus sieben abstrakten Bildern mit weichen Farbverläufen, Überlagerungen, sowie Blur- und Grain-Effekten besteht. Durch die Verwendung kontrastreicher und vibranter Farben strebten wir eine visuell ansprechende Abstraktion an. Aus diesen Ideen entwickelten wir ein Auge aus mehreren blury, abstrakten Kreisen, die mit unterschiedlichem Delay dem Cursor folgen.
Mehrere Kreise und Farbdesign
In unserer finalen Umsetzung entschieden wir uns für ein Farbdesign mit mehr Kontrast. Mehrere Kreise, die sich mit verschiedenem Delay bewegen, erzeugen eine dynamische und faszinierende Interaktion. Der schwarze Hintergrund verstärkt die Leuchtkraft der Farben und sorgt für eine dramatische Wirkung. Ursprünglich war noch Text und ein roter Kasten zur Orientierung vorhanden, die aber während des Designprozesses entfernt wurden. Trotz anfänglicher Herausforderungen und mehrfachen Experimentierens fanden wir eine geeignete Darstellung, die unsere Vision erfolgreich umsetzt.
Blur-Effekt
Um die Formen und Farben weiter zu abstrahieren und unser Moodboard näher zu kommen, integrierten wir einen Blur-Effekt. Dieser lässt die Formen und Farben verschwimmen und verstärkt die abstrakte Ästhetik. Bei der Farbauswahl entschieden wir uns für Rot und Magenta, da diese Farben eine bedrohliche Atmosphäre schaffen und den Blick einfangen. Wir stellten fest, dass viele der zuvor verwendeten Farben schlecht mit dem Beamer projiziert wurden, daher mussten wir unsere Farbpalette anpassen, um die gewünschte Wirkung zu erzielen.
Durch diese kreativen und technischen Anpassungen gelang es uns, ein Ausstellungsstück zu schaffen, das das Auge auf eine neue, abstrakte Weise darstellt und den Betrachtern ein unheimliches und faszinierendes Erlebnis bietet.
Aufbau und Animation
Der Code erstellt mehrere Kreise, die ein Auge darstellen, indem er Variablen zur Bestimmung der Größe, Farbe und Position der Kreise initialisiert. Die äußeren und inneren Kreise werden in Arrays gespeichert, die ihre Positionen und Versätze festlegen. In der draw-Funktion wird der äußere Kreis (body) auf die Mausposition ausgerichtet, während die inneren Kreise (k1, k2, ...) sich innerhalb des äußeren Kreises bewegen. Die Kreise verkleinern sich, je weiter sie vom äußeren Kreis entfernt sind, indem der Durchmesser jedes Kreises schrittweise mit einem Faktor von 0.8 multipliziert wird. Dies erzeugt einen Effekt, bei dem die Kreise nach innen hin kleiner werden, wodurch die Illusion eines Auges entsteht.
p5.js | AnimationIris und Zufall
Der letzte Kreis fungiert als Iris des Auges und hat eine eigene Position und Transparenz. In der setup-Funktion werden die Ausgangs- und Zielpositionen dieses letzten Kreises auf die Mitte der Leinwand gesetzt. Die Position der Iris ändert sich zufällig jede Sekunde, indem ein neuer Zielpunkt in der Nähe der Mausposition berechnet wird. Dies wird später ausgetauscht durch die Tracking-Funktion. Diese Zielposition wird innerhalb des äußeren Kreises beschränkt, sodass die Iris sich nicht über den Rand hinaus bewegt. Die Bewegung der Iris zur neuen Zielposition erfolgt schrittweise mit einem Easing-Faktor, wodurch eine sanfte, langsame Animation entsteht.
p5.js | IrisDas gesamte Auge mit Mauscursor
Der Aufbau des Auges beginnt mit der Erstellung mehrerer Kreise, wobei der äußere Kreis die Basis darstellt und die inneren Kreise sowie die Iris darauf aufbauen. Die Iris wird durch den letzten Kreis repräsentiert, dessen Position zufällig um die Mausposition variiert wird. Diese zufällige Bewegung wird durch eine Zielposition bestimmt, die innerhalb des äußeren Kreises bleibt und mit einem Easing-Faktor sanft erreicht wird, wodurch die Iris der Bewegung des Cursors folgt. Der Blur-Effekt wird auf die Kreise angewendet, um eine sanfte Darstellung des Auges zu erzeugen. Das gesamte Auge, einschließlich des äußeren Kreises, der inneren Kreise und der Iris, ist so gruppiert, dass es als Einheit der Bewegung des Mauszeigers folgt.
Die Funktion des Verfolgens des Mauszeigers beginnt damit, dass bodyX
und bodyY
, die Positionen des äußeren Kreises, mit einem Easing-Faktor (bodyEasing
) auf mouseX
und mouseY
zusteuern. Die inneren Kreise (kX
und kY
) berechnen ihre Zielpositionen basierend auf bodyX
, bodyY
und ihren festen Versätzen (kOffsetX
, kOffsetY
), und bewegen sich ebenfalls mit einem Easing-Faktor (kEasing
) dorthin. Die Zielposition der Iris (targetLastCircleX
, targetLastCircleY
) wird zufällig um die Mausposition variiert und bleibt innerhalb des äußeren Kreises, wobei sie mit einem eigenen Easing-Faktor (lastCircleEasing
) erreicht wird. Dadurch bewegen sich lastCircleX
und lastCircleY
sanft auf die neue Zielposition zu. So folgt das gesamte Auge, einschließlich aller Kreise, der Bewegung des Mauszeigers als eine Einheit. Die Mausfunktion soll nun im nächsten Schritt durch die Tracking-Funktion von ml5.js ersetzt werden.
let numCircles = 4, colors = [], baseAngle = 0;
let eyeSize = 350; // Variable zur Bestimmung der Größe des Auges
let circleColor = [255, 0, 255]; // Variable zur Bestimmung der Farbe der Kreise
let lastCircleColor = [0, 0, 0]; // Variable zur Bestimmung der Farbe des letzten Kreises
let lastCircleTransparency = 250; // Variable zur Bestimmung der Transparenz des letzten Kreises
let bodyX, bodyY; // Position des äußeren Kreises (body)
let kX = [], kY = []; // Positionen der inneren Kreise (k1, k2, ...)
let kOffsetX = [], kOffsetY = []; // Permanente Versätze der inneren Kreise
let bodyEasing = 0.004; // Verzögerungsfaktor für body
let kEasing = 0.008; // Verzögerungsfaktor für k1, k2, ...
let lastCircleX, lastCircleY; // Position des letzten Kreises
let targetLastCircleX, targetLastCircleY; // Zielposition für den letzten Kreis
let lastCircleEasing = 0.06; // Verzögerungsfaktor für die Position des letzten Kreises
let bodyBuffer, irisBuffer; // Separate PGraphics-Objekte für Blur-Effekte
function setup() {
createCanvas(800, 600);
noStroke();
// Erstelle PGraphics-Objekte
bodyBuffer = createGraphics(width, height);
irisBuffer = createGraphics(width, height);
// Setup für den ersten Teil des Codes (body und k1, k2, ...)
bodyX = width / 2;
bodyY = height / 2;
for (let i = 0; i < numCircles; i++) {
let blueShade = map(i, 0, numCircles - 1, 255, 0);
let whiteShade = map(i, 0, numCircles - 1, 0, 255);
colors.push(color(circleColor[0], circleColor[1], blueShade, whiteShade));
kX.push(bodyX);
kY.push(bodyY);
kOffsetX.push(random(-eyeSize * 0.01, eyeSize * 0.01)); // Fester Versatz
kOffsetY.push(random(-eyeSize * 0.01, eyeSize * 0.01)); // Fester Versatz
}
// Setup für den zweiten Teil des Codes (letzter Kreis)
lastCircleX = width / 2;
lastCircleY = height / 2;
targetLastCircleX = lastCircleX;
targetLastCircleY = lastCircleY;
}
function draw() {
background(0); // Hintergrund schwarz
// Verfolge die Maus mit dem äußeren Kreis (body) mit einer Verzögerung
let targetBodyX = mouseX;
let targetBodyY = mouseY;
bodyX += (targetBodyX - bodyX) * bodyEasing;
bodyY += (targetBodyY - bodyY) * bodyEasing;
// Begrenze die Bewegung des äußeren Kreises innerhalb des Canvas
let maxDiameter = eyeSize;
let bodyRadius = maxDiameter / 2;
bodyX = constrain(bodyX, bodyRadius, width - bodyRadius);
bodyY = constrain(bodyY, bodyRadius, height - bodyRadius);
// Zeichne die Kreise, die das Auge bilden (body und k1, k2, ...) in bodyBuffer
bodyBuffer.clear();
for (let i = numCircles - 1; i >= 0; i--) {
let angle = baseAngle + i * 0.1;
let diameter = maxDiameter * 0.5 * (1 + 0.5 + sin(angle) * 0.25);
if (i == 0) {
// Äußerer Kreis (body)
bodyBuffer.fill(red(colors[i]), green(colors[i]), blue(colors[i]), 100);
bodyBuffer.ellipse(bodyX, bodyY, diameter);
} else {
// Innere Kreise (k1, k2, ...)
let targetKX = bodyX + kOffsetX[i];
let targetKY = bodyY + kOffsetY[i];
kX[i] += (targetKX - kX[i]) * kEasing;
kY[i] += (targetKY - kY[i]) * kEasing;
// Berechne den Abstand zwischen dem Mittelpunkt des äußeren Kreises und dem inneren Kreis
let distance = dist(bodyX, bodyY, kX[i], kY[i]);
if (distance > maxDiameter / 2 - diameter / 2) {
let angle = atan2(kY[i] - bodyY, kX[i] - bodyX);
kX[i] = bodyX + (maxDiameter / 2 - diameter / 2) * cos(angle);
kY[i] = bodyY + (maxDiameter / 2 - diameter / 2) * sin(angle);
}
bodyBuffer.fill(red(colors[i]), green(colors[i]), blue(colors[i]), map(diameter, 0, maxDiameter, 0, 100));
bodyBuffer.ellipse(kX[i], kY[i], diameter);
}
maxDiameter *= 0.8;
}
// Füge den Blur-Effekt für den äußeren Kreis und die inneren Kreise hinzu
let blurAmountBody = map(eyeSize, 100, 500, 1, 10); // Passen Sie den Bereich nach Bedarf an
bodyBuffer.filter(BLUR, blurAmountBody);
image(bodyBuffer, 0, 0);
// Berechne eine neue Zielposition für den letzten Kreis
if (frameCount % 80 == 0) { // Ändere die Zielposition jede Sekunde
let offsetX = random(-maxDiameter / 8, maxDiameter / 8);
let offsetY = random(-maxDiameter / 8, maxDiameter / 8);
targetLastCircleX = mouseX + offsetX;
targetLastCircleY = mouseY + offsetY;
// Begrenze die Bewegung innerhalb des äußeren Kreises
let distance = dist(bodyX, bodyY, targetLastCircleX, targetLastCircleY);
let maxDistance = maxDiameter / 2 - maxDiameter * 0.1 / 2; // Kleinerer Spielraum für zufällige Bewegung
if (distance > maxDistance) {
let angle = atan2(targetLastCircleY - bodyY, targetLastCircleX - bodyX);
targetLastCircleX = bodyX + maxDistance * cos(angle);
targetLastCircleY = bodyY + maxDistance * sin(angle);
}
}
// Bewege den letzten Kreis zur Zielposition mit Easing
lastCircleX += (targetLastCircleX - lastCircleX) * lastCircleEasing;
lastCircleY += (targetLastCircleY - lastCircleY) * lastCircleEasing;
// Zeichne den letzten Kreis mit Blur-Effekt in irisBuffer
irisBuffer.clear();
let maxDiameterLast = eyeSize * 0.25; // Kleinerer Durchmesser für den letzten Kreis
let angleLast = baseAngle + (numCircles - 1) * 0.2;
let diameterLast = maxDiameterLast * 0.5 * (1 + 0.5 + sin(angleLast) * 0.25);
irisBuffer.fill(lastCircleColor[0], lastCircleColor[1], lastCircleColor[2], lastCircleTransparency);
irisBuffer.ellipse(lastCircleX, lastCircleY, diameterLast);
// Füge den Blur-Effekt für den letzten Kreis hinzu
let blurAmountLastCircle = map(diameterLast, 1, maxDiameterLast, 2, 10); // Passen Sie den Bereich nach Bedarf an
irisBuffer.filter(BLUR, blurAmountLastCircle);
image(irisBuffer, 0, 0);
baseAngle += 0.01; // Animationsparameter erhöhen
}
Umsetztung in p5.js und ml5.js
Generative Kreise
Unser Ziel war die Generierung von Kreisen in verschiedenen Größen, die sich nicht überschneiden, um als Barriere oder Augenhöhle zu dienen. Wir legten die Anzahl der Kreise mit let numCircles = 100;
fest und speicherten die Kreise im circles
Array.
Augenbewegung
Zur Erstellung des Auges verwendeten wir drei Kreise (schwarz, weiß, blau). Die weißen und blauen Kreise folgen dem Cursor, wobei der blaue Kreis immer innerhalb des weißen Kreises bleibt. Die Bewegungen werden in der draw()
Funktion dargestellt, die den schwarzen Kreis zeichnet und die Position des blauen Kreises basierend auf der Mausbewegung aktualisiert.
Personenerkennung mit ml5.js
ml5.js ist eine JavaScript-Bibliothek basierend auf TensorFlow.js, die die Integration von maschinellem Lernen in Webanwendungen erleichtert. Sie bietet vortrainierte Modelle für verschiedene Aufgaben und ist speziell für Webanwendungen entwickelt, was sie kompatibel mit p5.js macht. Die Einfachheit der Nutzung und die schnelle Prototypenerstellung machen ml5.js besonders sinnvoll für unser Projekt.
Personenanzahl und Augenanpassung
Ziel war die Erkennung und Zählung von Personen im Video. Das Video wird kontinuierlich auf Personen untersucht. Für jede erkannte Person wird ein Auge erzeugt, das auf die Mitte des Erkennungsrechtecks ausgerichtet ist. Die Anzahl der erkannten Personen bestimmt die Anzahl der Augen.
Augen folgen den Personen
Die Augen werden so positioniert, dass sie sich nicht überschneiden. Die inneren weißen und blauen Kreise (die Pupillen) folgen der Bewegung der Personen im Video. Um eine neue Bewegungsachse einzuführen und die Tiefe zu simulieren, skalieren wir die Augen basierend auf der Nähe der Person zur Webcam/Projektion. Je näher die Person, desto größer das Auge.
Ausbalancieren des Trackings:
Um zu verstehen, wie das Ausbalancieren des Trackings die Bewegung der Augen smoother (weicher) macht, schauen wir uns die Glättungsfaktoren und die Schwellenwerte an, die in deinem Code verwendet werden. Insbesondere geht es um die Variablen smoothFactorOuter, smoothFactorInner und movementThreshold.
Die Variablen
smoothFactorOuter = 0.005; – Glättungsfaktor für die äußeren Kreise (äußere Augenbewegung).
smoothFactorInner = 0.01; – Glättungsfaktor für die inneren Kreise (innere Augenbewegung).
movementThreshold = 25; – Bewegungsschwellenwert.
Funktion smoothEyes
Die Funktion smoothEyes ist dafür zuständig, die Positionen der Augen zu glätten. Hier wird die Glättung der Bewegungen in den äußeren und inneren Kreisen angewendet:
function smoothEyes(newEyes, existingEyes, factorOuter, factorInner, threshold) {
return newEyes.map((newEye, index) => {
let existingEye = existingEyes[index] || newEye;
let dx = abs(existingEye.x - newEye.x);
let dy = abs(existingEye.y - newEye.y);
// Zufällige Versatzwerte für eine leichte Bewegung
let offsetX = (random(-1, 1) * newEye.diameter) / 4;
let offsetY = (random(-1, 1) * newEye.diameter) / 4;
// Wenn die Bewegung innerhalb des Schwellenwerts liegt, behalten wir die alte Position bei
if (dx < threshold && dy < threshold) {
return existingEye;
}
// Anwendung der Glättung auf die neuen Positionen
return {
xOuter: lerp(existingEye.xOuter || existingEye.x, newEye.x + offsetX, factorOuter),
yOuter: lerp(existingEye.yOuter || existingEye.y, newEye.y + offsetY, factorOuter),
xInner: lerp(existingEye.xInner || existingEye.x, newEye.x + offsetX, factorInner),
yInner: lerp(existingEye.yInner || existingEye.y, newEye.y + offsetY, factorInner),
diameter: newEye.diameter,
creationTime: existingEye.creationTime
};
});
}
Erklärung der Glättung
Lerping: Die Funktion lerp(a, b, f)
(linear interpolation) nimmt zwei Werte a
und b
sowie einen Faktor f
und berechnet einen Wert zwischen a
und b
, basierend auf f
. Wenn f
klein ist, bleibt der Wert näher bei a
, was zu einer langsameren Bewegung führt. Das ergibt eine weiche Übergangsbewegung.
SmoothFactorOuter und SmoothFactorInner: Diese bestimmen, wie stark die neue Position mit der alten Position gemischt wird. Ein kleinerer Glättungsfaktor führt zu einer langsameren Anpassung an die neue Position, was die Bewegung glatter macht.
MovementThreshold: Dieser Schwellenwert bestimmt, ob die Bewegung groß genug ist, um eine Anpassung vorzunehmen. Wenn die Bewegung unter diesem Schwellenwert liegt, wird die alte Position beibehalten, wodurch kleine, eventuell ruckartige Bewegungen ignoriert werden.
Visualisierung der Augen
In der Funktion drawEyes
wird die geglättete Position verwendet, um die Augen zu zeichnen. Die äußeren und inneren Kreise der Augen werden basierend auf den geglätteten Positionen gezeichnet, was zu einer weichen, fließenden Bewegung führt.
function drawEyes(eyes) {
for (let i = 0; i < eyes.length; i++) {
let eye = eyes[i];
fill(0);
noStroke();
ellipse(eye.xOuter, eye.yOuter, eye.diameter, eye.diameter);
let innerDiameter = eye.diameter / 2;
let maxOffset = (eye.diameter - innerDiameter) / 2;
fill(255,0,77);
ellipse(eye.xOuter, eye.yOuter, innerDiameter, innerDiameter);
let largerWhiteDiameter = innerDiameter * 1.3;
fill(255,0,103, 150);
ellipse(eye.xOuter, eye.yOuter, largerWhiteDiameter, largerWhiteDiameter);
let blueDiameter = innerDiameter * 0.7;
let blueMaxOffset = (innerDiameter - blueDiameter) / 2;
let blueOffsetX = constrain(eye.xInner - eye.xOuter, -blueMaxOffset, blueMaxOffset);
let blueOffsetY = constrain(eye.yInner - eye.yOuter, -blueMaxOffset, blueMaxOffset);
fill(253,0,255);
ellipse(eye.xInner + blueOffsetX, eye.yInner + blueOffsetY, blueDiameter, blueDiameter);
let largerBlueDiameter1 = blueDiameter * 0.3;
let largerBlueDiameter2 = blueDiameter * 0.5;
let speedFactor = map(blueDiameter, innerDiameter * 0.7, innerDiameter * 1.6, 1.5, 0.5);
fill(0, 0, 0);
ellipse(eye.xInner + blueOffsetX * speedFactor, eye.yInner + blueOffsetY * speedFactor, largerBlueDiameter1, largerBlueDiameter1);
ellipse(eye.xInner + blueOffsetX * (speedFactor * 0.5), eye.yInner + blueOffsetY * (speedFactor * 0.5), largerBlueDiameter2, largerBlueDiameter2);
}
}
Zusammenfassung
Die Kombination aus smoothFactorOuter
, smoothFactorInner
und movementThreshold
ermöglicht es, die Bewegungen der Augen zu glätten, indem kleinere Bewegungen ignoriert und größere Bewegungen verlangsamt werden. Dies führt zu einer weicheren, weniger ruckartigen Darstellung der Augenbewegungen.
Anfangsanimation und Sound
Um Nutzer anzulocken, wird ein pulsierendes Auge als Anfangsanimation abgespielt, solange keine Person erkannt wird. Ein unheimlicher Sound unterstützt den Look & Feel, begleitet von genereller Hintergrundmusik. Ein Effektsound signalisiert den Übergang von der Anfangsanimation zum Hauptprogramm.
Um mp3 in p5.js einzubetten müssen jene Dateien in den Projektordner geladen werden und wie folgt verlinkt werden. Außerdem ist eine Einbettung ein p5.js sound library notwendig.
<!-- Einbindung von p5.js Soundbibliothek -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.4.0/addons/p5.sound.min.js"></script>
// Musik
let bgMusic;
// Soundeffekt für die erste Person
let firstPersonSound;
function preload() {
bgMusic = loadSound('hintergrund.mp3');
firstPersonSound = loadSound('effekt.mp3');
}
---
function setup() {
// Starte die Hintergrundmusik und lasse sie loopen
bgMusic.loop();
}
Anfangsanimation in p5jsZusammenfügen zum Finalen Prototypen
Beim Zusammenfügen der Einzelnen Elemente haben wir alle Farben und den Blur Effekt angepasst sodas die Augen unterinander stimmig sind. Das ist die Angepasste Animation von zuvor.
button zu p5 Version 1 button zu p5 Final VersionSchlussfolgerung
Dieses Projekt vertiefte unser Verständnis für Interaktionsdesign durch die Kombination von Technologie und Visualisierung, um die Besuchererfahrung zu prägen. Wir lernten, kreative Probleme zu lösen und Prototypen zu entwickeln, indem wir p5.js für visuelle Gestaltung und ml5.js für Personentracking nutzten. Diese Erfahrung förderte unsere Selbstständigkeit und Eigeninitiative, indem wir konditionierte Denkmuster überwanden und neue Ideen durch Experimentieren erkundeten. Bei Problemen halfen Teamarbeit und Ressourcen wie ChatGPT, alternative Lösungen zu finden, wodurch wir Flexibilität und Lernfähigkeit verbesserten.