Interaktiver Canvas-Effekt: Schwingungen und Gravitation

Interaktiver Canvas-Effekt: Schwingungen und Gravitation
Von Lars Ebert am 15.09.14, 11:00
Kategorien: JavaScript, Programmieren and Tutorials

Achtung: Physik und Mathematik voraus! In diesem Artikel zeige ich Dir, wie man mit Physik und ein wenig JavaScript einem Canvas Leben einhauchen kann. Der entstandene Effekt kann, in abgewandelter Form, sehr schön in eine Webseite eingebunden werden.

Was haben wir vor?

Diese »Simulation« zeigt, wie man mit nur zwei physikalischen Prinzipien einen interessanten und interaktiven Effekt erzeugen kann. Im Verlauf des Artikels erkläre ich kurz die physikalischen Prinzipien, die hinter diesem Effekt stehen, danach machen wir uns ans Werk, den Effekt selbst zu erstellen.

Physik I: Anziehung zwischen zwei Körpern

Das erste physikalische Prinzip, welches ich in der Erstellung des Effektes genutzt habe, ist die Anziehung zwischen zwei Körpern. Hinweis: Ich habe nicht Physik Studiert und meine Tage im Physik-Unterricht liegen auch schon ein paar Jahre zurück. Ich gehe hier nicht im Detail auf die Physik ein und vereinfache die Formeln an einigen Stellen..

Die Anziehungskraft zwischen zwei Körpern kann man deutlich in unserem Sonnensystem sehen. Sie ist die Kraft, die den Mond auf seiner Bahn um die Erde hält und die Erde auf der Umlaufbahn um die Sonne. Durch die gleiche Kraft kommt bei uns auf der Erde auch die Erdanziehungskraft zustande.

Schaut man sich noch einmal die Demo oben an, kann man sehen, dass die Punkte von dem Mauszeiger abgestoßen werden. Ich habe hier (physikalisch nicht ganz korrekt) die Anziehungskraft einfach umgekehrt, damit die Punkte abgestoßen werden, nicht angezogen. Dies funktioniert dann quasi, als wenn man zwei Magneten verkehrt herum aneinander hält—sie stoßen sich ab.

Die Anziehungskraft (oder in diesem Fall »Abstoßungskraft«) ist von drei Faktoren abhängig: der Masse der beiden Körper und dem Abstand zwischen den Körpern. Je größer die Masse der beiden Körper ist, desto stärker ist die Anziehungskraft. Je weiter die Körper auseinander sind, desto mehr nimmt die Kraft ab. Dies lässt sich durch die folgende Formel abbilden:

$$F_G = G \frac{m_1 * m_2}{r^2}$$

\(m_1\) und \(m_2\) sind die Massen der beiden Körper und \(r\) ist die Entfernung zwischen den beiden Körpern. \(G\) ist die universelle Gravitationskonstante, sie stellt eine Relation zwischen Masse, Entfernung und Kraft dar. In unserem Fall lassen wir die Konstante einfach weg. Physikalisch ist dies zwar sehr falsch, aber hier geht es nicht um perfekte Physik und das genaue Verhältnis zwischen Masse und Kraft ist in diesem Fall nicht so wichtig, denn wir definieren später die Masse der Punkte so oder so nach belieben. Die für uns relevante Formel ist also:

$$F_G = \frac{m_{Punkt} * m_{Cursor}}{r^2}$$

Physik II: Von der Kraft zur Bewegung

Als nächstes müssen wir mit dieser Kraft, die wir jetzt berechnen können, irgendwie die Strecke herausfinden, um die sich die Punkte bewegen. Dazu werfen wir erst einmal einen Blick auf den Zusammenhang zwischen Bewegung, Geschwindigkeit und Beschleunigung.

Wenn sich der Standpunkt \(s\) eines Körpers ändert, also eine Bewegung stattfindet, liegt eine Geschwindigkeit vor. Geschwindigkeit, Strecke und Zeit stehen im folgenden Verhältnis:

$$s = v * t$$

Das ist vergleichbar mit der Bewegung eines Autos: Wenn ein Auto 30 Sekunden lang eine Geschwindigkeit von 30 Metern pro Sekunde fährt, legt es insgesamt eine Strecke von \(s = 30 m/s * 30 s = 900 m\) zurück. Wenn ein Auto in 60 Sekunden 1200 Meter zurücklegt, beträgt die Geschwindigkeit \(v = \frac{1200 m}{60 s} = 20 m/s\). Soweit die Beziehung zwischen Strecke und Zeit.

Analog kann man eine ähnliche Beziehung zwischen der Geschwindigkeit und Beschleunigung herstellen:

$$v = a * t$$

Auch hier wieder der Vergleich mit einem Auto: Beschleunigt ein Auto von 0 Metern pro Sekunde auf 30 Meter pro Sekunde in 5 Sekunden, beträgt die Beschleunigung \(a = \frac{30 m/s}{5 s} = 6 m/s^2\). Beschleunigt ein Auto 20 Sekunden lang pro Sekunde um 3 Meter pro Sekunde, nimmt die Geschwindigkeit um \(v = 3 m/s^2 * 20 s = 60 m/s\) zu.

Als letztes brauchen wir noch die Beziehung zwischen der Kraft, mit der ein Körper beschleunigt wird, und der resultierenden Beschleunigung:

$$F = a * m$$

Hier kommt wieder die Masse des zu beschleunigenden Körpers, also unseres Punktes, ins Spiel. Je schwerer der Körper ist, desto mehr Kraft wird benötigt, um ihn in Bewegung zu versetzen. Das kannst Du leicht nachvollziehen: Einen Tennisball zu werfen ist viel leichter als eine Bowlingkugel zu werfen.

Diese verschiedenen Beziehungen können wir jetzt nutzen, um eine Abstoßung zwischen den Punkten und unserem Cursor zu simulieren.

Grundgerüst aus HTML und JavaScript

Bevor wir mit dem JavaScript-Code beginnen können, brauchen wir zunächst eine Leinwand:

<!DOCTYPE html>
<html>
<head>
	<title>Physik Swing</title>
</head>
<body>
	<canvas></canvas>
	<script type="text/javascript" src="js/jquery-2.1.1.min.js"></script>
	<script type="text/javascript" src="js/main.js"></script>
</body>
</html>

Nun folgt unser JavaScript. Als erstes definieren wir einige Eigenschaften der Simulation, nämlich die Höhe und Breite der Leinwand, den Abstand und Radius der Punkte und die Anzahl der Reihen und Spalten von Punkten.

var width = 500, height = 500;
var rythm = 12, radius = 3;
var rows = 10, columns = 10;

Nun erstellen wir eine Animation. Ich benutze dazu gerne die Funktion window.requestAnimationFrame(), denn damit kann man wesentlich flüssigere Animationen erreichen. Allerdings ist diese Funktion nicht in jedem Browser vorhanden, also brauchen wir ein Fallback.

if(!window.requestAnimationFrame) {
	window.requestAnimationFrame = ( function() {
		return window.webkitRequestAnimationFrame ||
		window.mozRequestAnimationFrame ||
		window.oRequestAnimationFrame ||
		window.msRequestAnimationFrame ||
		function(callback, element) {
			window.setTimeout(callback, 1000 / 60);
		};
	})();
}

Nun bereiten wir schon einmal alles für die Animation vor.

var lastFrameTime = null;
var mousePosition = null;
var mouseDown = false;

var $canvas = $('canvas');
$canvas.attr('width', width).attr('height', height).css({
	margin: '0px auto'
});

var context = $canvas.get(0).getContext('2d');

$('body').css({
	textAlign: 'center',
	background: '#111111',
	margin: '0px'
}).mousemove(function(e) {
	var fixPoint = $canvas.offset();
	
	mousePosition = {
		x: e.pageX - fixPoint.left,
		y: e.pageY - fixPoint.top
	};
}).mousedown(function() {
	mouseDown = true;
}).mouseup(function() {
	mouseDown = false;
});

Hier passiert eininges: Wir definieren uns ein paar Variablen, um den Zeitpunkt des letzten Frames und den Status der Maus zu speichern. Wir geben dem Canvas Breite und Höhe und legen ein bisschen CSS fest. Wir fragen den Context des Canvas ab. Abschließend geben wir dem Body noch einige CSS-Eigenschaften und binden an ihn drei Event-Handlers, die den Maus-Status speichern, sobald sich dieser ändert.

var dots = [];
for(var x = 0; x < columns; x++) {
	for(var y = 0; y < rows; y++) {
		var x0 = (width - rows * rythm) / 2 + x * rythm;
		var y0 = (height - columns * rythm) / 2 + y * rythm;
		
		dots.push({
			x: x0,
			y: y0,
			x0: x0,
			y0: y0,
			vx: 0,
			vy: 0
		});
	}
}

loop();

Nun generieren wir ein Array mit den Punkten. Jeder Punkt bekommt eine Ursprungs-Koordinate \((x_0|y_0)\), eine aktuelle Position \((x|y)\) und einen Geschwindigkeits-Vektor \((v_x|v_y)\). Warum müssen wir die Geschwindigkeit auch speichern? Wenn der Cursor später eine Kraft auf einen Punkt ausübt, wird dieser beschleunigt, nimmt also an Geschwindigkeit zu. Bewegt man dann den Cursor weg, bleibt der Punkt natürlich nicht sofort regungslos stehen, sondern bewegt sich durch seine Trägheit weiter. Deshalb müssen wir hier die »angesammelte« Geschwindigkeit speichern.

In der letzten Zeile rufen wir schließlich die Funktion loop() auf. Diese Funktion stellt quasi die Animations schleife dar, für jeden Frame neu durchlaufen wird. Diese Funktion definieren wir jetzt.

function loop() {
	var thisFrameTime = new Date().getTime();
	if(lastFrameTime != null) {
		update(thisFrameTime - lastFrameTime);
	}
	lastFrameTime = thisFrameTime;
	
	draw();
	
	window.requestAnimationFrame(loop);
}

Als erstes generieren wir die Zeit des aktuellen Frames, damit können wir die Zeit berechnen, die seit dem letzten Frame vergangen ist. Dies ist später die Zeit, die wir in unsere Formeln für Beschleunigung und Geschwindigkeit einsetzen können. Wenn lastFrameTime noch null ist, befinden wir uns grade im ersten Frame, deshalb muss die Funktion update() noch nicht ausgeführt werden. Ansonsten berechnet diese Funktion die Beschleunigung und Bewegung für den verstrichenen Zeitraum und aktualisieren die Punkte entsprechend. Anschließend wird die Funktion draw() ausgeführt, diese zeichnet den Canvas neu. Zuletzt bewirkt der Aufruf von window.requestAnimationFrame(loop), dass die Funktion loop() vor dem nächsten aktualisieren des Bildschirms erneut aufgerufen wird.

function update(deltaTime) {
	
}

function draw() {
	context.clearRect(0, 0, width, height);
	
	for(var i = 0; i < dots.length; i++) {
		context.fillStyle = '#ffffff';
		
		context.beginPath();
		context.arc(dots[i].x, dots[i].y, radius, 0, 2 * Math.PI);
		context.fill();
	}
}

Die Funktion update() tut vorerst gar nichts, erst kümmern wir uns darum, dass die Punkte korrekt auf dem Canvas angezeigt werden. Dazu dient die Funktion draw().

Hier wird zunächst die Leinwand geleert, danach wird jeder Punkt einzeln an seiner aktuellen Position \((x|y)\) ausgegeben.

Bewegung und Beschleunigung

So sieht unsere Simulation momentan aus. Die Punkte bewegen sich natürlich nicht, da wir die update()-Funktion noch nicht geschrieben haben. Das kommt jetzt! Als erstes definieren wir ein paar neue Konstanten:

var dotMass = 5, cursorMass = 1000000;
var frictionCoefficient = .95;

Hier legen wir die Masse des Cursors und der einzelnen Punkte fest. Diese Werte habe ich durch ein wenig experimentieren herausgefunden. Durch Anpassen dieser Werte können wir das Verhalten der Simulation anpassen. Außerdem legen wir die Reibung fest, damit die Punkte auch wieder Geschwindigkeit verlieren.

Nun schreiben wir die update()-Funktion.

function update(deltaTime) {
	for(var i = 0; i < dots.length; i++) {
		var mouseAccelerationVector = {
			x: 0,
			y: 0
		};
		if(mousePosition != null) {
			var distanceSquared = Math.pow(mousePosition.x - dots[i].x, 2) + Math.pow(mousePosition.y - dots[i].y, 2);
			var distance = Math.sqrt(distanceSquared);
			var mouseAcceleration = (mouseDown ? 4 : 1) * cursorMass / distanceSquared;
			mouseAccelerationVector.x = mouseAcceleration * (dots[i].x - mousePosition.x) / distance;
			mouseAccelerationVector.y = mouseAcceleration * (dots[i].y - mousePosition.y) / distance;
		}
		
		dots[i].vx += mouseAccelerationVector.x * deltaTime / 1000;
		dots[i].vy += mouseAccelerationVector.y * deltaTime / 1000;
		
		dots[i].vx *= frictionCoefficient;
		dots[i].vy *= frictionCoefficient;
		
		dots[i].x += dots[i].vx * deltaTime / 1000;
		dots[i].y += dots[i].vy * deltaTime / 1000;
	}
}

Für jeden Punkt müssen wir die Änderungen getrennt berechnen. Wenn die Maus-Position bisher unbekannt ist, findet keine Beschleunigung statt. Kennen wir die Maus-Position jedoch, können wir die Beschleunigung einfach berechnen. Wir erinnern uns an die Formeln:

$$F = \frac{m_{cursor} * m_{punkt}}{r^2}$$ $$F = a * m_punkt$$

\(F\) beschreibt hier in beiden Formeln die gleiche Kraft, und zwar die Kraft, die der Cursor auf einen Punkt auswirkt. Deshalb können wir die Formeln kombinieren, um eine Formel für die Beschleunigung \(a\) zu erhalten.

$$a = \frac{m_{cursor} * m_{punkt}}{r^2 * m_{punkt}} = \frac{m_{cursor}}{r^2}$$

Die Entfernung \(r\) können wir einfach aus den Koordinaten der Maus und des Punktes berechnen:

$$r^2 = (x_{maus} - x_{punkt})^2 + (y_{maus} - {y_punkt)^2$$

Das ist genau, was wir in der Variable distanceSquared speichern. Wir speichern hier das Quadrat der Entfernung, da wir die Entfernung später so oder so als Quadrat einsetzen müssen. Später brauchen wir jedoch die Entfernung auch noch unquadriert, deshalb ziehen wir in der nächsten Zeile aus distanceSquared die Wurzel.

Mit unserer Formel können wir jetzt einfach die Beschleunigung berechnen, diese speichern wir in mouseAcceleration. Wenn die Maus gedrückt ist, wird die Masse des Cursors vervierfacht, so kann der Nutzer hinterher auch noch per Klick mit dem Effekt interagieren.

Aus dieser Gesamt-Beschleunigung können wir jetzt die Beschleunigung in X- und Y-Richtung ermitteln. Da die Beschleunigung von der Entfernung abhängig ist, können wir uns folgender Formel bedienen:

$$\frac{x}{r} = \frac{a_x}{a}$$ $$\frac{y}{r} = \frac{a_y}{a}$$

So können wir die Werte für mouseAccelerationVector berechnen.

Um welchen Betrag sich die Geschwindigkeit ändert, können wir mit dieser Formel berechnen:

$$v = a * t$$

Hier setzen wir als Zeit deltaTime in Sekunden ein. Die berechneten Werte addieren wir anschließend zu dots[i].vx und dots[i].vy.

Als nächstes multiplizieren wir die Geschwindigkeit mit dem Reibungswert, damit die Punkte sich nicht ewig weiter bewegen.

Zuletzt können wir die Änderung der Position mit dieser Formel berechnen, auch hier setzen wir wieder deltaTime ein.

$$s = v * t$$

Physik III: Federschwingung

So, jetzt können wir mit dem Cursor die Punkte abstoßen. Es gibt nur ein Problem: die Punkte kommen nicht zurück. Um dies jetzt zu ändern, müssen wir uns wieder der Physik zuwenden.

Stellen wir uns einmal vor, die Punkte wären mit einem Gummiband oder einer Feder an ihrer Original-Position befestigt. Bewegt man die Punkte nun von dieser Position weg, zwingt die Feder sie zurück zu ihrer Original-Position. Allerdings vollzieht sich dieser Vorgang nicht linear, auf dem Weg zur Original-Position nimmt der Punkt an Geschwindigkeit zu und schießt so übers Ziel hinaus. So pendelt die Bewegung eine Weile um die Originalposition. Das klingt zwar sehr komplex, allerdings lässt sich dies durch eine sehr einfache Formel ausdrücken:

$$F = D * s$$

\(F\) ist hier die Kraft, die die Feder auf den Punkt ausübt und \(D\) ist die Federkonstante. Diese ist für jede Feder anders, wir können diese später einfach als Konstante definieren. \(s\) ist die Entfernung des Punktes von seiner Original-Position. Diese Formel sagt also einfach nur aus, dass die ausgeübte Kraft umso stärker ist, je weiter der Punkt von seiner Original-Position entfernt ist. Auch diese Formel kann man in der Praxis beobachten: Zieht man ein Gummiband oder eine Feder in die Länge, muss man um so stärker ziehen, je weiter das Band oder die Feder schon ausgedehnt ist.

Anwendung der Feder-Kraft

Da wir bereits wissen, wie wir aus einer Kraft eine Beschleunigung und daraus die Geschwindigkeit und Bewegung berechnen, können wir unsere update()-Funktion jetzt einfach anpassen, um den federnden Effekt zu realisieren.

Zunächst definieren wir die oben erwähnte Feder-Konstante.

var featherConstant = 300;

Als nächstes arbeiten wir die Feder-Kraft in die update()-Funktion ein.

function update(deltaTime) {
	for(var i = 0; i < dots.length; i++) {
		var mouseAccelerationVector = {
			x: 0,
			y: 0
		};
		if(mousePosition != null) {
			var distanceSquared = Math.pow(mousePosition.x - dots[i].x, 2) + Math.pow(mousePosition.y - dots[i].y, 2);
			var distance = Math.sqrt(distanceSquared);
			var mouseAcceleration = (mouseDown ? 4 : 1) * cursorMass / distanceSquared;
			mouseAccelerationVector.x = mouseAcceleration * (dots[i].x - mousePosition.x) / distance;
			mouseAccelerationVector.y = mouseAcceleration * (dots[i].y - mousePosition.y) / distance;
		}
		
		var featherAccelerationVector = {
			x: 0,
			y: 0
		};
		var amplitude = Math.sqrt(Math.pow(dots[i].x - dots[i].x0, 2) + Math.pow(dots[i].y - dots[i].y0, 2));
		if(amplitude != 0) {
			var featherAcceleration = featherConstant * amplitude / dotMass;
			featherAccelerationVector.x = featherAcceleration * (dots[i].x0 - dots[i].x) / amplitude;
			featherAccelerationVector.y = featherAcceleration * (dots[i].y0 - dots[i].y) / amplitude;
		}
		
		dots[i].vx += (mouseAccelerationVector.x + featherAccelerationVector.x) * deltaTime / 1000;
		dots[i].vy += (mouseAccelerationVector.y + featherAccelerationVector.y) * deltaTime / 1000;
		
		dots[i].vx *= frictionCoefficient;
		dots[i].vy *= frictionCoefficient;
		
		dots[i].x += dots[i].vx * deltaTime / 1000;
		dots[i].y += dots[i].vy * deltaTime / 1000;
	}
}

Auch hier ist die Federbeschleunigung erst einmal 0, denn wenn der Punkt sich noch nicht vom Original-Punkt bewegt hat, wirkt auch noch keine Kraft. Anschließend berechnen wir die Amplitude, also die Entfernung vom Original-Punkt. Dazu können wir wieder \(r^2 = x^2 + y^2\) nutzen. Wenn die Amplitude nicht 0 ist, berechnen wir aus ihr die Kraft und daraus die Beschleunigung des Punktes und speichern dies in featherAcceleration. Anschließend teilen wir, wie schon bei der Maus-Beschleunigung, diese Beschleunigung in X- und Y-Komponente auf.

Bei der Änderung der Geschwindigkeit addieren wir nun die beiden wirkenden Beschleunigungen und bringen somit die Federschwingung in die Simulation ein.

Einfache Formeln, viel Interaktivität

Damit ist unsere Simulation komplett. Mit nur ein paar einfachen physikalischen Formeln konnten wir schon eine relativ komplexe Interaktion erreichen. Vielleicht hat dies nicht unbedingt viele praktische Anwendungsmöglichkeiten, aber mit dieser oder einer abgewandelten Technik kann man zumindest einen interessanten Effekt erreichen.

Nach einer viel zu langen Schreibpause ist dies mein erster Artikel. Ich freue mich auf Euer Feedback. Was macht ihr mit dem Canvas? Jetzt seid ihr dran!