Rolf B: Element mit Javascript/CSS und transform in einem Bogen verschieben

Beitrag lesen

Hallo T-Rex,

ich hab da mal ein bisschen gespielt. Basis meiner Spielerei ist requestAnimationFrame, und ich habe dieser Funktion eine Art "Animation Scheduler" als Callback gegeben. Dieser Callback bekommt den aktuellen Animationszeitpunkt (in Millisekunden) als double übergeben. Der Callback muss requestAnimationFrame erneut aufrufen, damit weiter animiert wird (ähnlich settimeout). Das sieht grundsätzlich so aus:

let animObjects = [];
window.requestAnimationFrame(scheduleAnimations);

function scheduleAnimations(timer) {
  let l = animObjects.length;
  for (let i=0; i<l; i++) {
     let s = animObjects[i];
     if (!s.state && s.startTime <= timer) {
        s.state = 1;
        s.init(timer);
     }
     if (s.state == 1) {
        let done = s.animate(timer);
        if (!done) s.state = 2;
     }
  }
  window.requestAnimationFrame(scheduleAnimations);
}

Zunächst mal völlig unfähig, irgendwas zu tun, solange keine Objekte im animObjects Array liegen. Von so einem Objekt werden 4 Dinge erwartet:

  • Bereitstellung einer state-Eigenschaft, die initial undefined oder 0 sein kann (irgendwie falsy)
  • Bereitstellung einer startTime Eigenschaft
  • Bereitstellung einer init-Methode, die einen timer-Wert erhält
  • Bereitstellung einer animate-Methode, die ebenfalls den timer-Wert erhält. animate muss true zurückgeben, wenn das Objekt weiter an der Animation teilnehmen soll.

Der Scheduler prüft, ob der State eines Animationsobjekts falsy ist und die bestellte Startzeit erreicht ist. Wenn ja, setzt der den State auf 1 und ruft die init-Methode auf. Solange der State auf 1 bleibt, nimmt das Objekt am Animationszyklus teil. Dazu wird die animate-Methode aufgerufen und der aktuelle Timerwert übergeben. Wenn animate false zurückgibt, wird state auf 2 gesetzt und damit ist die Animation vorbei. Ich verwende ganz bewusst eine for-Schleife und nicht forEach(), weil es ja durchaus sein kann dass als Reaktion auf animate oder init ein neues Animationsobjekt entsteht, und das will ich erst im nächsten Durchlauf dabei haben.

Das ist ein total simples, aber unglaublich mächtiges Tool, sobald intelligente animierbare Objekt ins Spiel kommen.

Wie kommt jetzt was in dieses Array mit Animations-Objekten hinein? In meiner Spielerei habe ich dafür ein weiteres Animationsobjekt verwendet:

let animGenerator = {
   startTime: 0,
   toCreate: 50,            // eigenes Property, warum auch nicht? 
   init: function(t0) {
     this.nextStart = t0;   // nextStart ist ebenfalls ein eigenes Property
   },
   animate: function(t) {
      if (t >= this.nextStart) {
         this.nextStart += 200;
         let c = new Coin(imgSrc, this.nextStart);
         animObjects.push(c);
         this.toCreate--;
       }
       return this.toCreate> 0;
    }
};
animObjects.push(animGenerator);

Der AnimGenerator erzeugt im 200ms Takt 50 Coins und fügt sie in animObjects ein. Danach deaktiviert er sich.

So. Aber was ist nun das Coin-Objekt? Natürlich wieder eines, das sich vom Scheduler steuern lässt. Aber diesmal etwas umfangreicher. Ich habe die ECMAScript 2015 Syntax zur Klassendefinition verwendet.

let canvas = document.getElementById("canvas");
let timeOut = document.getElementById("timeOut");
let imgSrc = '...'; // URL für das Animationsbild

class Coin {
   constructor(elem, t0) {
      let img = document.createElement("img");
      img.className ="coin";
      img.src = imgSrc;
      img.hidden = true;
      this.element = img;
      canvas.appendChild(img);
      this.startTime = t0;
      this.x0 = -20;
      this.y0 = 0;
   }
   init(t) {
       this.t0 = t;
       this.f = (Math.random() + 2) * Math.PI;
       this.element.hidden = false;
    }
    animate(t) {
       let dt = t - this.t0;
       let dx = dt/10; // 100 Pixel pro Sekunde
       let dy = Math.abs(Math.cos(dt/1000*this.f))*(1000-dx)/4;
       //console.log("Move " + this.element.id + " to ("+dx+","+dy+")");
       this.element.style.left = (dx + this.x0) + "px";
       this.element.style.bottom = (dy + this.y0) + "px";
       if (dx < 980) return true;
    }
}

Die Arbeit wird von der animate-Methode in Coin gemacht, sie berechnet, wie lange das Objekt schon unterwegs ist (dt) und daraus die gewünschte x- und y-Position. Die X-Bewegung ist linear von links nach rechts, die Y-Bewegung wird über den Betrag des Cosinus zu einer Hüpfbewegung. f ist die Hüpffrequenz, die ich zufällig ermittle. Da der Betrag des Cosinus Werte von 0-1 liefert, muss ich das hochskalieren um eine sichtbare Bewegung zu erhalten. Diese Skalierung mache ich abfallend (1000-dx), ok, eigentlich müsste das eine E-Funktion sein um physikalisch korrekt zu sein, aber das war mir jetzt egal. 1000-dx ist zu Beginn 1000, das ist mir zu viel, drum teile ich durch 4.

Das ganze braucht etwas HTML:

<div id="canvas"><div>

und CSS:

#canvas {
  position:relative;
  width: 1000px; height: 500px; 
  background-color: #336;
  border: 4px solid #ccf;
}

#canvas img.coin {
  position: absolute; width: 48px; height: 48px;
}

Meine CPU macht keinen Unterschied zwischen 5 und 50 hüpfenden Images!

JSFiddle dazu: https://jsfiddle.net/Rolf_b/9shgxk04/

Rolf

--
sumpsi - posui - clusi