Tuesday, June 10, 2014

Snap.svg Animations in Polymer


Up tonight, I see if Snap.svg can be of some help with the topping animation in the <x-pizza> Polymer element that builds awesome pizzas for order:



As for the previous four posts, I again tack on “Polymer” to the title, but so far there have been no special considerations in Snap.svg code for working with Polymer elements. It just feels like Snap.svg coding.

Snap.svg coding is better than raw JavaScript SVG coding, but I do not know if it is leaps and bounds above raw JavaScript SVG coding when working with SVG assets. I am using a Polymer core-* element (<core-ajax>) to load the assets so I am eschewing some of the benefits of Snap.svg: the Snap.load() function. My hope is that animations are where Snap.svg will really shine for me.

Consider (but don't look too closely, it'll burn your eyes) my current animation code:
  _addFirstHalfTopping: function(maker) {
    var group = document.createElementNS("http://www.w3.org/2000/svg", "g");
    group.setAttribute('transform', 'translate(0, 150)');

    for (var i=0; i<20; i++) {
      var angle = 2 * Math.PI * Math.random();
      var u = Math.random() + Math.random();
      var distance = 125 * ((u < 1.0) ? u : 2-u);

      var topping = maker();

      var g = document.createElementNS("http://www.w3.org/2000/svg", "g");

      g.setAttribute(
        'transform',
        'translate(' +
          (-Math.abs(distance * Math.sin(angle))) + ', ' +
          (distance * Math.cos(angle)) +
        ')'
      );

      g.appendChild(topping);
      group.appendChild(g);
    }

    this.$['pizza'].appendChild(group);

    var start_y;
    var dur_y = 500;
    function animateY(time) {
      if (start_y == null) start_y = time;
      if (time - start_y > dur_y) return;

      var x = 80;
      var y = 75 + 10 * Math.sin(0.5 * Math.PI * (time - start_y) / dur_y);

      group.setAttribute('transform', 'translate(' + x + ', ' + y + ')');

      requestAnimationFrame(animateY);
    }

    var start_x;
    var dur_x = 1500;
    function animateX(time) {
      if (start_x == null) start_x = time;
      if (time - start_x > dur_x) return requestAnimationFrame(animateY);


      var x = 80 * (time - start_x) / dur_x;
      var y = 75;
      group.setAttribute('transform', 'translate(' + x + ', ' + y + ')');

      requestAnimationFrame(animateX);
    };

    requestAnimationFrame(animateX);
  },
That is 50+ lines of ugliness. It creates a group of pizza toppings, animates the group sliding in from the left, then down onto the pizza. Look closely if you dare. That is what those 50+ lines do.

To be sure, Snap.svg can help with creation of the element at the code—no more document.createElementNS(), thank you very much. There is also a little benefit to be realized from the initial attributes and translation that is done. I did very much the same last night. I will stick with the same randomizing code at the top. The result of that initial work leaves the start of _addFirstHalfTopping() looking like this:
  _addFirstHalfTopping: function(maker) {
    var group = this.snap.group();
    for (var i=0; i<20; i++) {
      var angle = 2 * Math.PI * Math.random();
      var u = Math.random() + Math.random();
      var distance = 125 * ((u < 1.0) ? u : 2-u);
      var t = new Snap.Matrix().
        translate(
          -Math.abs(distance * Math.sin(angle)),
          (distance * Math.cos(angle))
        );
      var topping = maker.call(this);
      topping.transform(t);
      group.add(topping);
    }
    // ...
  }
Not a huge win, but a definite cleanup.

This leave the focus of cleanup tonight on the animateX() and animateY() functions. The former animates the toppings gliding in from stage left and then calls animateY() to gently place the pizza toppings on the pizza. In other words, I have chained animations that I would like to carry over to Snap.svg.

The original DOM / requestAnimationFrame() implementation of animateX() is:
    var start_x;
    var dur_x = 1500;
    function animateX(time) {
      if (start_x == null) start_x = time;
      if (time - start_x > dur_x) return requestAnimationFrame(animateY);


      var x = 80 * (time - start_x) / dur_x;
      var y = 75;
      group.setAttribute('transform', 'translate(' + x + ', ' + y + ')');

      requestAnimationFrame(animateX);
    }

    requestAnimationFrame(animateX);
There is a lot of noise in there, but it boils down to incrementally translating the x position over time. And here Snap.svg give me a big win. With Snap.svg, I do not need the noise—keeping track of the time, invoking requestAnimationFrame(), checking for the end of recursive calls—none of it. Instead, I simply supply an end location for the translation and the duration:
    group.animate(
      {transform: 'translate(135, 130)'},
      2*1000
    );
How cool is that?

Chaining SVG animations is not quite as nice, but still loads better than defining animateY(). To invoke a Snap.svg animation after another has completed, I have to supply a fourth argument to group.animate()—a callback function that animates the next leg of the motion:
    group.animate(
        {transform: 'translate(135, 120)'},
        2*1000,
        mina.linear,
        function() {
          group.animate(
            {transform: 'translate(140, 130)'},
            1*1000
          );
        }
      );
In addition to the callback function that animates the toppings down, I have to supply a third argument to the outer animate()—an easing function. I just stick with linear here. Aside from the easing function and the callback, the next animation is just as clean as the first—I supply the end point translation and the time that it should take to get there. Then Snap.svg does the rest. That is nearly 30 lines of code reduced to 11 very readable lines.

While it might be nicer if it really was a chain (or if animate() returned a promise), this is still a big win over what I had pre-Snap.svg.

Day #90

No comments:

Post a Comment