Saturday, January 18, 2014

Day 1000: Animated SVG in Polymer


For an ostensibly UI facing library, I have done almost no UI work while researching Patterns in Polymer. I think that speaks to the hidden power of coding with the library. Even so, it is a web based, UI library, so I ought to do some UI-like things. Enter last night's <x-pizza> tag—the SVG based pizza builder:



The pizza SVG in this Polymer is built by passing a “maker” function, which makes individual toppings (e.g. pepperoni), into add-topping methods:
@CustomTag('x-pizza')
class XPizza extends PolymerElement {
  // ...
  _addFirstHalfTopping(maker) {
    for (var i=0; i<20; i++) {
      var angle = 2 * PI * _rand.nextDouble();
      var u = _rand.nextDouble() + _rand.nextDouble();
      var distance = 125 * ((u < 1.0) ? u : 2-u);

      var topping = maker()
        ..attributes.addAll({
            'cx': "${150 - (distance * sin(angle)).abs()}",
            'cy': "${150 + (distance * cos(angle))}"
          });

      $['pizza-graphic'].append(topping);
    }
  }
  // ...
}
It is a little hard to randomly distribute things in a circle, but happily there is Stack Overflow, which is the source for most of that.

It is nice to see the pizza built and all, but I would like to give hungry buyers more of a sense of building their own pizzas. Animating the toppings coming in from the side and dropping onto the pizza should do that nicely. I have no idea if that is possible with SVG, but there is one way to find out...

I start by grouping the toppings in an SVG <g> element. It turns out that you cannot set cx and cy attributes on group elements. Instead you have to translate them in a transform (that's gonna be easy to remember):
  _addFirstHalfTopping(maker) {
    var group = new GElement()
      ..attributes = {'transform': 'translate(150,150)'};

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

      var topping = maker()
        ..attributes.addAll({
            'cx': "${-(distance * sin(angle)).abs()}",
            'cy': "${ (distance * cos(angle))}"
          });

      group.append(topping);
    }
    $['pizza-graphic'].append(group);
  }
I have my toppings grouped and can position them together. How about animating?

Unfortunately, the <animate*> tags do not seem to work too well when trying to dynamically start animations. The <animateMotion> tag almost works… some of the time… when you don't hit reload too fast or stare at the page funny:
  _addFirstHalfaTopping(maker) {
    // ...
    $['pizza-graphic'].append(group);
    group..append(
          new AnimateMotionElement()..attributes = {
            'from': '0, 150',
            'to':   '150, 150',
            'dur':  '3s',
            'fill': 'freeze'
          }
        );

  }
For whatever reason, that does not seem reliable and fails to work at all on Firefox. So it seems that I need to revisit my old friend requestAnimationFrame(). I have honestly been missing this since 3D Game Programming for Kids went to press, so I may be reaching for a golden hammer here. But I don't care, animation frames, which are functions that get called when the browser signals it is ready to paint, are cool.

It seems that Dart has a future-based take on animation frames. Given an animate() function, I can ask the browser to call it with window.animationFrame.then(animate). The function that animates pizza toppings sliding in from the side over the course of 1.5 seconds is then:
  _addFirstHalfaTopping(maker) {
    // ...
    $['pizza-graphic'].append(group);
    var start;
    int dur = 1500;
    animate(time) {
      if (start == null) start = time;
      if (time - start > dur) return;
      var x = 150 * (time - start) / dur;
      group.attributes['transform'] = 'translate(${x.toInt()}, 150)';

      window.animationFrame.then(animate);
    };
    window.animationFrame.then(animate);
  }
That does the trick. Just as with the JavaScript requestAnimationFrame(), I have to request the animate() function once and then recursively call it from within animate() afterwards to keep the animation running. The effect is a nice, smooth animation that starts with the toppings floating in from the side:



And settling down nicely:



For completeness' sake, I animate the toppings falling gently, like a mother putting a babe down to rest, at the end of the left-to-right animation:
  _addFirstHalfaTopping(maker) {
    // ...
    $['pizza-graphic'].append(group);
    var start_y;
    int dur_y = 500;
    animateY(time) {
      if (start_y == null) start_y = time;
      if (time - start_y > dur_y) return;

      var x = 150;
      var y = 140 + 10 * sin(0.5 * PI * (time - start_y) / dur_y);

      group.attributes['transform'] = 'translate(${x}, ${y.toInt()})';

      window.animationFrame.then(animateY);
    }

    var start_x;
    int dur_x = 1500;
    animateX(time) {
      if (start_x == null) start_x = time;
      if (time - start_x > dur_x) return window.animationFrame.then(animateY);
      // ...
    };

    window.animationFrame.then(animateX);
  }
My code has serious longer-than-my-arm code smell at this point, but it works!

I call it a night here. Up tomorrow either the JavaScript implementation of this SVG Polymer or, if that proves trivial, I will move on to playing with Angular and Polymer together.

Day #1000

3 comments:

  1. Hey, 1000 days, congratulations! Keep up the great chain - I enjoy reading along.

    ReplyDelete
    Replies
    1. Much thanks! I'm pretty excited to finally reach this milestone :)

      Delete
  2. I've been finding your site to be so useful while learning Polymer.dart. Thanks so much!

    ReplyDelete