In diesem Tutorial soll beschrieben werden, wie man den elastischen Stoß von Kugeln unterschiedlicher Massen mit Hilfe von Javascript elementar, d. h. ohne Verwendung von Frameworks, umsetzen kann.
Zum Einsatz kommen die Canvas-Zeichenfläche von HTML5 und die in Javascript integrierten Funktionen zum Zeichnen auf dieser Canvas-Zeichenfläche wie z. B. arc, fill, clearRect und requestAnimationFrame.
Eine voll funktionsfähige Demo-Seite (einzelne HTML-Datei) mit obigem Animationsfenster und ausführlich kommentiertem Javascript-Code gibt es hier:
Einfach die Seite aufrufen und den Seitenquelltext kopieren.
Die Herleitung der verwendeten Formeln beim Zusammenstoßen der Kugeln ist hier ausführlich beschrieben:
Herleitung der Formeln des elastischen Stoßes zweier Kugeln
class Ball { constructor (x, y, r, m, vx, vy, col, ctx) { this.x = x; // x-Position this.y = y; // y-Position this.r = r; // Radius this.m = m; // Masse this.vx = vx; // x-Komponente der Geschwindigkeit this.vy = vy; // y-Komponente der Geschwindigkeit this.col = col; // Füllfarbe des Balls this.ctx = ctx; // ein Zeiger (pointer) zum Zeichnungskontext } proofBorder() { // Diese Methode der Klasse 'Ball' berechnet die Geschwindigkeitsänderungen // beim Anstoßen an die Begrenzungen der Fläche var dx; var dy; if (this.y <= this.r) { // wenn die Kugel außerhalb der oberen Begrenzung ist, // wird sie zuerst wieder vollständig in die Zeichenfläche zurück verschoben. // Sonst kann es passieren, dass sie diesen Schritt nicht mehr schafft, // weil bei der nächsten Berechnung wieder eine Berührung mit der Begrenzung registriert // wird und die Geschwindigkeit wieder fälschlicherweise die Richtung wechselt. // Die Kugel kommt dann nicht mehr von der Bande los, sondern zittert hin und her. dy = this.r - this.y + 1; dx = dy * this.vx / this.vy; this.x += dx; this.y += dy; // Hier kommt die eigentliche Reaktion auf die Berührung der Begrenzung (die Reflexion) this.vy = -this.vy; } if (this.y >= canvas.height - this.r) { dy = this.y + this.r - canvas.height + 1; dx = dy * this.vx / this.vy; this.x -= dx; this.y -= dy; this.vy = -this.vy; } if (this.x <= this.r) { dx = this.r - this.x + 1; dy = dx * this.vy / this.vx; this.x += dx; this.y += dy; this.vx = -this.vx; } if (this.x >= canvas.width - this.r) { dx = this.x + this.r - canvas.width + 1; dy = dx * this.vy / this.vx; this.x -= dx; this.y -= dy; this.vx = -this.vx; } } show (dt) { // Diese Methode der Klasse 'Ball' berechnet die neue Position des Balls // und zeichnet ihn an die neue Position this.proofBorder(); this.x += this.vx * dt; this.y += this.vy * dt; ctx.strokeStyle = this.col; ctx.fillStyle = this.col; ctx.beginPath(); ctx.arc(Math.round(this.x), Math.round(this.y), this.r, 0, Math.PI * 2, true); //ctx.stroke(); ctx.fill(); } } // end of class Ball
function proofCollision(b1, b2, dt) { // Diese Funktion berechnet den Zusammenstoß zweier Kugeln // Parameter: // b1: Kugel 1 // b2: Kugel 2 // dt: Anzahl der Millisekunden, die seit der letzten Positionsberechnungen vergangen ist // Zuerst wird geprüft, ob die beiden Bälle einen geringeren Abstand haben als die Summe ihrer Radien if ((b1.x - b2.x) * (b1.x - b2.x) + (b1.y - b2.y) * (b1.y - b2.y) <= (b1.r + b2.r) * (b1.r + b2.r)) { var phi; if (b1.x == b2.x) { // Spezialfall b1.x == b2.x (die Kugeln befinden sich senkrecht übereinander) phi = b2.y > b1.y ? Math.PI/2 : -Math.PI/2; } else { // Hier muss b1.x <> b2.x sein! phi = Math.atan((b2.y - b1.y)/(b2.x - b1.x)); } // Rechenterme, die in den Formeln mehrfach auftreten, werden erst einmal in Variablen gespeichert var sinphi = Math.sin(phi); var cosphi = Math.cos(phi); var v1xsinphi = b1.vx * sinphi; var v1xcosphi = b1.vx * cosphi; var v1ysinphi = b1.vy * sinphi; var v1ycosphi = b1.vy * cosphi; var v2xsinphi = b2.vx * sinphi; var v2xcosphi = b2.vx * cosphi; var v2ysinphi = b2.vy * sinphi; var v2ycosphi = b2.vy * cosphi; var v1zaehler = (b1.m - b2.m) * (v1xcosphi + v1ysinphi) + 2 * b2.m * (v2xcosphi + v2ysinphi); var v2zaehler = (b2.m - b1.m) * (v2xcosphi + v2ysinphi) + 2 * b1.m * (v1xcosphi + v1ysinphi); var msum = b1.m + b2.m; // Berechnung der neuen Geschwindigkeiten // (Die Bezeichnungen sind in Anlehnung an die Formeln gewählt, die in der ausführlichen // Herleitung - siehe PDF-Datei - gewählt wurden) b1.vx = (v1xsinphi - v1ycosphi) * sinphi + v1zaehler * cosphi / msum; b1.vy = (-v1xsinphi + v1ycosphi) * cosphi + v1zaehler * sinphi / msum; b2.vx = (v2xsinphi - v2ycosphi) * sinphi + v2zaehler * cosphi / msum; b2.vy = (-v2xsinphi + v2ycosphi) * cosphi + v2zaehler * sinphi / msum; // Wenn sich zwei Kugeln überlappen, müssen sie zuerst wieder getrennt werden, bevor es weiter geht. // Sonst kann es vorkommen, dass sie gar nicht mehr auseinander kommen, weil beim nächsten Schritt // wieder eine Überlappung (ein Stoß) registriert wird und die Richtung der Geschwindigkeiten // erneut geändert wird while ((b1.x - b2.x) * (b1.x - b2.x) + (b1.y - b2.y) * (b1.y - b2.y) <= (b1.r + b2.r) * (b1.r + b2.r)) { b1.x += b1.vx * dt; b1.y += b1.vy * dt; b2.x += b2.vx * dt; b2.y += b2.vy * dt; } } }
Die Funktion proofCollision ist das Herzstück der Javascript-Anwendung. Hier werden die Formeln des elastischen Stoßes, deren Herleitung in der obigen PDF-Datei dargestellt ist, in Javascript-Code umgesetzt.
function oneStep() { ctx.clearRect(0, 0, canvas.width, canvas.height); // Die Zeichenfläche wird komplett gelöscht for (var i = 0; i < ball.length; i++) { // Alle Kugeln werden auf Kollision mit einer anderen for (var k = i + 1; k < ball.length; k++) { // geprüft proofCollision(ball[i], ball[k], dt); // und ggf. die Geschwindigkeiten neu berechnet } ball[i].show(dt); // Die Kugeln werden an den neuen Positionen gezeichnet } } function draw() { timeNew = new Date(); // die momentane Zeit wird genommen dt = (timeNew - timeOld) * timefactor; // Die Zeitdifferenz 'dt' und der Zeitfaktor 'timefactor' bestimmen, // wie weit die Kugeln fortbewegt werden. if (dt == 0) { dt = 1 // der Fall dt=0 soll vermieden werden } timeOld = timeNew; // die neue Zeit wird als 'alte' gespeichert oneStep(); // ein Animationsschritt wird durchgeführt // steps++; // die Aktivierung dieser Zeile bewirkt, dass nach der durch 'stepLimit' festgelegten // Anzahl von Schritten gestopt wird if (running && (steps < stepLimit)) { // Wenn nicht gestoppt wurde, window.requestAnimationFrame(draw); // wird der nächste Animationsschritt abgerufen. } else { running = false; btnToggle.innerHTML = "START"; } }
Die Funktion draw wird bei jeder Neuberechnung aufgerufen. Sie sorgt dafür, dass die Zeichenfläche gelöscht und komplett neu gezeichnet wird.
Ein Teil der Funktion 'draw' wurde in die Funktion 'oneStep' ausgelagert,
weil dieser Teilcode nochmals in der Funktion
start (s. u.) zum Einsatz kommt.
function start() { // wird beim Klick auf den Button 'START/STOP' aufgerufen if (running) { running = false; btnToggle.innerHTML = "START"; oneStep(); } else { running = true; steps = 0; timeOld = new Date(); window.requestAnimationFrame(draw); btnToggle.innerHTML = "STOP"; } }
Die Funktion start wird bei einem Klick auf den Button START/STOP ausgeführt.
Die Animation kann auf diese Weise gestartet und angehalten werden.
var btnToggle = document.getElementById('toggle'); // der Button START/STOP const timefactor = 1; // eine Erhöhung dieses Wertes lässt die Animation schneller ablaufen // Diese Variable ist nicht notwendig, aber ganz praktisch: // wenn man mit Hilfe eines Buttons auf Zeitlupe umschalten möchte, // setzt man einfach timefactor = 0.2 const stepLimit = 5; // Wenn man in der function draw die Anweisung 'steps++' aktiviert, stoppt die Animation // nach jeweils 'stepLimit' Schritten var dt; // Diese Variable enthält die Anzahl der Millisekunden, // die seit der letzten Neuberechnung vergangen sind. ctx.lineWidth = 4; // Dicke der Begrenzungslinien var ball = []; // ein Array von Kugeln wird generiert // Hier werden Kugeln hinzugefügt: // (Bedeutung der Parameter siehe oben beim Konstruktor der Klasse 'Ball') ball.push(new Ball(200, 600, 125, 2, 0.2, -0.5, '#2044F0', ctx)); ball.push(new Ball(500, 150, 100, 1, -0.12, 0.3, '#E04460', ctx)); ball.push(new Ball(120, 150, 70, 0.7, 0.6, 0.2, '#44E460', ctx)); ball.push(new Ball(1100, 710, 50, 0.4, 0.5, 0.1, '#E4E460', ctx)); ball.push(new Ball(800, 710, 50, 0.4, 0.5, 0.1, '#E4E460', ctx)); // Hier können weitere Kugeln hinzugefügt werden: //ball.push(new Ball(1000, 380, 50, 0.4, 0.5, 0.1, '#E4E460', ctx)); //ball.push(new Ball(1000, 520, 50, 0.4, 0.5, 0.1, '#E4E460', ctx)); var running = false; // die Animation wird erst beim Klicken auf den Button 'START' gestartet var timeNew = new Date(); var timeOld = timeNew; // Die Differenz dieser beiden Zeiten (je nach Performance der CPU einige // Millisekunden) wird in der Variablen dt gespeichert. btnToggle.focus(); // Der Button START/STOP soll bei Aufruf der Seite den Fokus haben window.requestAnimationFrame(draw); // Dies ist nicht der Start der Animation, denn in der Funktion 'draw' // wird festgestellt, dass 'running == false' ist, und // 'requestAnimationFrame' wird nicht weiter aufgerufen. // Diese Anweisung dient zum einmaligen Zeichnen der Kugeln // nach dem Aufruf der Seite.
Die Animation läuft grundsätzlich so ab, dass die Funktion requestAnimationFrame des Windows-Objekts aufgerufen wird. Als Parameter wird die Funktion draw übergeben, die die Aufgabe hat, die Zeichenfläche zu aktualisieren. Damit danach die Animation weiterläuft, wird innerhalb der Funktion draw die Funktion requestAnimationFrame erneut aufgerufen.
Gelegentlich kann es zu Fehlverhalten von Kugeln kommen, wenn eine der Kugeln eine so hohe Geschwindigkeit bekommt, dass sie durch eine andere hindurch saust, ohne dass ein Zusammenstoß registriert werden konnte.
Ein weiterer kritischer Fall mit unvorhersehbarem Verhalten kann eintreten, wenn mehr als zwei Kugeln gleichzeitig zusammenstoßen.