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
Hey, 1000 days, congratulations! Keep up the great chain - I enjoy reading along.
ReplyDeleteMuch thanks! I'm pretty excited to finally reach this milestone :)
DeleteI've been finding your site to be so useful while learning Polymer.dart. Thanks so much!
ReplyDelete