Der elastische Stoß mit Javascript

Demo und Tutorial

Genereller Hinweis

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.

Demo-Seite

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.

Herleitung der Formeln

Die Herleitung der verwendeten Formeln beim Zusammenstoßen der Kugeln ist hier ausführlich beschrieben:

HTML-Code

<canvas id="canvas" width="1200" height="800"></canvas> Das ist die einzige Zeile, die im HTML-Code benötigt wird.
Dadurch wird eine Zeichenfläche angezeigt, auf der die x-Werte nach rechts und die y-Werte nach unten zunehmen.
Links oben ist der Punkt (0 | 0).

JAVASCRIPT: Die Klasse 'Ball'


  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
    

JAVASCRIPT: Die Funktion 'proofCollision'


  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.

JAVASCRIPT: Die Funktion 'draw'


  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.

JAVASCRIPT: Die Funktion 'start'


  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.

JAVASCRIPT: Start des Programms


  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.

Kritisches

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.