T-Rex: Element mit Javascript/CSS und transform in einem Bogen verschieben

Moin,

ich programmiere im Moment an einem Spiel. Dort sollen Spielsteine ins Spielfeld hüpfen. Ein Hüpfen ist ja nichts weiter wie ein verschieben von a nach b in einem Bogen.

Aktuell habe ich dafür sogar schon eine kleine externe Funktion. Dort kann man einen Startpunkt (x,y) und einen Endpunkt (x,y) setzen. Dann kann man noch einen Winkel (angle) und die weite der Kurve mit einer Länge angeben. Funktioniert wunderbar…

… nur benutzt diese Funktion left und top als Positionierung. Wird nur ein Spielstein währen des Spiels verschoben reicht das auch völlig aus. Aber wenn initial alle 40 Spielsteine ins Spielfeld hüpfen kommt sogar mein Notebook ins schwitzen (vom Smartphone will mal gar nicht reden). Da ist die Verschiebung mittels Transform natürlich viel flüssiger.

Jetzt die logische Frage - kennt jemand eine Bilbiothek mit der ich Spielsteine hüpfen lassen kann, die aber auf transform setzt. Alternativ wäre eventuell eine kleine eigene Funktion - wobei diese wiederum einen callback (ähnlich dem progress bei jquery-animate haben müsste). Habe mich selbst schon ca. 30 Minuten an einer eigenen Funktion versucht, hab aber keinen brauchbaren Bogen hinbekommen :(.

Gruß
◖ T◌Rex ◗
  1. Hallo T-Rex,

    das Problem mit transform ist, dass die Anzahl der verfügbaren Funktionen auf Geraden und Kreise beschränkt ist. Sicherlich kann man durch eine Kombination von Verschiebungen und Drehungen eine Art "Hüpfen" hinbekommen. Die Trajektorie wird dann aber ein Halbkreis oder ein Ellipsenbogen sein, keine Parabel.

    Ich denke, dass Du an dieser Stelle an der Grenzen von CSS Animationen angekommen bist. Der Browser führt nach solchen Animationen vermutlich viel zu viele Re-Layouts durch. Um solche Dinge flüssig laufen zu lassen, braucht man Hardware-Unterstützung durch die Grafikkarte,

    Hier hilft dir das <canvas> Element und die schon angesprochene requestAnimationFrame Funktion. Es gibt dazu einige Abschnitte in unserem Wiki, und auch Tutorials im MDN. Ich persönlich habe das noch nie benutzt und weiß darum nicht, wie effizient das Canvas-API ist.

    Rolf

    --
    sumpsi - posui - clusi
    1. Achso ... hab ich vergessen. Kein Canvas! Muss normales html sein.

      Ich denke mir das relativ simple. Wenn jemand es geschafft hat eine brauchbare Funktion via left und top Positionierung zu basteln, dann müsste man dieses left und top nur durch die transform Verschiebung ersetzen. Ergo wäre es möglich. Mittels css womöglich nicht, aber mittels jquery animate eventuell.

      Trotzdem danke für deine Antwort.

      Ach was ich auch noch vergessen habe - ich suche eigentlich sowas wie http://ricostacruz.com/jquery.transit/. Nur kann diese jQuery Erweiterung leider keine kurven 😟.

            u                 x
         r      ß          e 
      G             T - R
       
      
      1. Lieber T-Rex,

        vor einigen Jahren hatte ich einmal dieses Projekt angefangen... vielleicht findest Du etwas nützliches in seinem Code?

        Liebe Grüße,

        Felix Riesterer.

      2. Hallo T-Rex!

        Schön wieder von Dir zu hören!

        Achso ... hab ich vergessen. Kein Canvas! Muss normales html sein.

        Ich denke mir das relativ simple. Wenn jemand es geschafft hat eine brauchbare Funktion via left und top Positionierung zu basteln, dann müsste man dieses left und top nur durch die transform Verschiebung ersetzen. Ergo wäre es möglich. Mittels css womöglich nicht, aber mittels jquery animate eventuell.

        Hast du Dir mal die Web Animations API angeschaut?

        Hier kannst Du CSS-Eigenschaften mit JavaScript animieren.(Beispiel: Ball) Nur die Kurven müsstest Du selbst programmieren, wobei ich hier evtl doch Halb/ Viertelkreise mit abnehmenden Radii (Radiussen?) nehmen würde.

        Trotzdem danke für deine Antwort.

        Ach was ich auch noch vergessen habe - ich suche eigentlich sowas wie http://ricostacruz.com/jquery.transit/. Nur kann diese jQuery Erweiterung leider keine kurven 😟.

              u                 x
           r      ß          e 
        G             T - R
         
        

        Herzliche Grüße

        Matthias Scharwies

        --
        "I don’t make typos. I make new words."
        1. Hallo Matthias,

          Radii (Radiussen?)

          Radien. Sagt Onkel Konrad.

          Rolf

          --
          sumpsi - posui - clusi
          1. Servus!

            Hallo Matthias,

            Radii (Radiussen?)

            Radien. Sagt Onkel Konrad.

            Jetzt, wo du's sagst! VordieStirnklatsch

            Herzliche Grüße

            Matthias Scharwies

            --
            "I don’t make typos. I make new words."
        2. Ola Matthias,

          joa lang ist's her :). Da ich aber wieder mehr in Richtung Webentwicklung gehe, werde ich wohl wieder aktiver werden.

          Vielen Dank für deine Hilfe. Wie man Dinge mit und ohne css animiert ist mir klar und überhaupt kein Problem. Wie geschrieben habe ich mich selbst bereits an einer Funktion versucht. Dabei habe ich ein Element in einer geraden Strecke von a nach b fliegen lassen. zwischendrin habe ich dann ein margin-top gesetzt, dass bis zu 50% der Animation ansteigt und dann wieder abfällt. Heraus kam eher ein Berg als ein Halbkreis. Zudem funktioniert das nur, wenn die Bewegung horizontal verläuft. Vertikal würde man es überhaupt nicht sehen. Zudem ist das setzen des margin wieder sehr Performance unfreundlich. Besser wäre ein hineinrechnen in das transform.

          Also würde ich es selbst programmieren bräuchte ich noch eine zündende Idee wie man die Halbkreis Bewegung berechnet und zwar ohne rotieren des Objektes und ohne Canvas.

                      T-Rex
          Gruß margin-Top
          
  2. 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
    1. Lieber Rolf,

      ich bin beeindruckt! Magst Du das zu einem JS-Tutorial zusammenschreiben und ins Wiki stellen?

      Liebe Grüße,

      Felix Riesterer.

      1. Hallo Felix,

        bin dran, dauert etwas. Es fehlen noch ein paar OO Säuberungen. Als Frickl wird es vermutlich zu gewaltig, oder?

        Wo gehört es hin?

        Rolf

        --
        sumpsi - posui - clusi
  3. vielen Dank an dieser Stelle an Rolf. Ich hab dann am Ende doch einen eigenen Weg gefunden in dem ich das Plugin was ich bereits nutzte umgebaut habe.

    /*
     * jQuery css bezier animation support -- Jonah Fox
     * version 0.0.1
     * Released under the MIT license.
     */
    /*
      var path = $.path.bezier({
        start: {x:10, y:10, angle: 20, length: 0.3},
        end:   {x:20, y:30, angle: -20, length: 0.2}
      })
      $("myobj").animate({path: path}, duration)
    
    */
    
    ;(function($){
    
      $.path = {};
    
      var V = {
        rotate: function(p, degrees) {
          var radians = degrees * Math.PI / 180,
            c = Math.cos(radians),
            s = Math.sin(radians);
          return [c*p[0] - s*p[1], s*p[0] + c*p[1]];
        },
        scale: function(p, n) {
          return [n*p[0], n*p[1]];
        },
        add: function(a, b) {
          return [a[0]+b[0], a[1]+b[1]];
        },
        minus: function(a, b) {
          return [a[0]-b[0], a[1]-b[1]];
        }
      };
    
      $.path.bezier = function( params, rotate ) {
        params.start = $.extend( {angle: 0, length: 0.3333}, params.start );
        params.end = $.extend( {angle: 0, length: 0.3333}, params.end );
    
        this.p1 = [params.start.x, params.start.y];
        this.p4 = [params.end.x, params.end.y];
    
        var v14 = V.minus( this.p4, this.p1 ),
          v12 = V.scale( v14, params.start.length ),
          v41 = V.scale( v14, -1 ),
          v43 = V.scale( v41, params.end.length );
    
        v12 = V.rotate( v12, params.start.angle );
        this.p2 = V.add( this.p1, v12 );
    
        v43 = V.rotate(v43, params.end.angle );
        this.p3 = V.add( this.p4, v43 );
    
        this.f1 = function(t) { return (t*t*t); };
        this.f2 = function(t) { return (3*t*t*(1-t)); };
        this.f3 = function(t) { return (3*t*(1-t)*(1-t)); };
        this.f4 = function(t) { return ((1-t)*(1-t)*(1-t)); };
    
        /* p from 0 to 1 */
        this.css = function(p) {
          var f1 = this.f1(p), f2 = this.f2(p), f3 = this.f3(p), f4=this.f4(p), css = {};
          if (rotate) {
            css.prevX = this.x;
            css.prevY = this.y;
          }
          css.x = this.x = ( this.p1[0]*f1 + this.p2[0]*f2 +this.p3[0]*f3 + this.p4[0]*f4 +.5 )|0;
          css.y = this.y = ( this.p1[1]*f1 + this.p2[1]*f2 +this.p3[1]*f3 + this.p4[1]*f4 +.5 )|0;
          css.left = css.x - params.start.x;
          css.top = css.y - params.start.y;
          //~ css.left = css.x + "px";
          //~ css.top = css.y + "px";
          return css;
        };
      };
    
      $.path.arc = function(params, rotate) {
        for ( var i in params ) {
          this[i] = params[i];
        }
    
        this.dir = this.dir || 1;
    
        while ( this.start > this.end && this.dir > 0 ) {
          this.start -= 360;
        }
    
        while ( this.start < this.end && this.dir < 0 ) {
          this.start += 360;
        }
    
        this.css = function(p) {
          var a = ( this.start * (p ) + this.end * (1-(p )) ) * Math.PI / 180,
            css = {};
    
          if (rotate) {
            css.prevX = this.x;
            css.prevY = this.y;
          }
          css.x = this.x = ( Math.sin(a) * this.radius + this.center[0] +.5 )|0;
          css.y = this.y = ( Math.cos(a) * this.radius + this.center[1] +.5 )|0;
          css.left = css.x + "px";
          css.top = css.y + "px";
          return css;
        };
      };
    
      $.fx.step.path = function(fx) {
        var css = fx.end.css( 1 - fx.pos );
        if ( css.prevX != null ) {
          $.cssHooks.transform.set( fx.elem, "rotate(" + Math.atan2(css.prevY - css.y, css.prevX - css.x) + ")" );
        }
        //~ fx.elem.style.top = css.top;
        //~ fx.elem.style.left = css.left;
        
        fx.elem.style.transform = "translate(" + css.left  + "px, " + css.top  + "px)";
        fx.elem.setAttribute("transform", fx.elem.style.transform );
      };
    
    })($);
    

    In den Kommentaren ganz oben steht wie man es benutzt. Man definiert erst einen path in dem man start und ende festlegt. Beide seiten können einen Winkel beinhalten. Damit steuert man die Krümmung. Ich denke das Muster entspricht der Kubische Bézierkurven (n=3)

    Am Ende kann man den Path für eine jquery Animation nutzen. Innerhalb der Funktion habe ich noch ein setAttribute dazu gebaut. So kann man bei einer Animation noch weitere transform werte wie z.B. sclae hinzu setzen ohne die Matrix bearbeiten zu müssen, die man bekommen würde, wenn man einfach nur den style.transform des Objektes holt. Ist ein bisschen praktischer.

    Diese kleine Lib habe ich nicht gebaut, sondern wie gesagt bislang benutzt und jetzt erfolgreich umgebaut. Wobei ... die Methode arc geht immer noch auf top und left. Anscheinend benutzte ich diese nicht 🐵.

    Gruß
    Bezir-Rex