Animating the YouTube Arrow

Posted in JavaScript, Web

Whenever I drift into one of my twice‐weekly YouTube stream‐of‐consciousness video‐watching binges, I let myself be bothered by a small, inconsequential nitpick.

Here’s the “Related Videos” heading in its collapsed state:

And here it is in its expanded state:

Though there’s no animation between these two states, I’ve always considered the metaphor to be a lot like the turning of a key in a lock. In other words, this arrow (or “norgie,” to use a term that I hate) should be rotating about its center. But that’s clearly not the case here. The arrow is rotated ninety degrees, but it also seems like it’s been moved up.

(Like I said, this is inconsequential, but I obsess about the inconsequential like it was my job, so we shall continue.)

A bit more care could be paid here. One approach, though it would work only in WebKit and Chrome (edit: and Firefox 3.1), would be to use the experimental -webkit-transform property to do a true rotation of the image. And this rotation could be synced with a slide‐down effect for the container below, bringing the effect much closer to what you’d see in a desktop app.

There isn’t a “rotate” animation built into script.aculo.us, but we can write one very easily:

Effect.Rotate = function(element, deltaDeg, options) {
  element = $(element);

  // Get the current rotation
  var currentDeg = 0;
  var transform = element.getStyle('-webkit-transform');
  var match = transform.match(/rotate\((\d+)deg\)/);
  if (match) {
    currentDeg = Number(match[1]);
  }

  return new Effect.Tween(element, currentDeg, currentDeg + deltaDeg, options,
    function(pos) {
      this.setStyle("-webkit-transform: rotate(" + pos + "deg);");
    }
  );
};

This code uses the new‐ish Effect.Tween, a generic effect container that accepts a function to do the animating. So we can pass this an element, the amount to rotate by (in degrees, with positive numbers rotating clockwise), and whatever options an effect would normally accept.

Now we’ll write some markup for a heading and its toggle‐able container:

<h1 id="heading">
  <img src="images/arrow.png" width="7" height="13" alt="" />
  Related Videos
</h1>

<div id="related" style="display: none">
  <div>
    <ul>
      <li>Lorem</li>
      <li>Ipsum</li>
      <li>Dolor</li>
    </ul>
  </div>
</div>

Notice the extra div wrapping the list. Effect.SlideDown and Effect.SlideUp need that extra unstyled div so that they don’t “jump” at the beginning and end of an animation.

And, finally, we need a function to respond when the user clicks on that heading:

function toggleExpansion(event) {
  var img = event.element().down('img');
  var isExpanded = $('related').visible();

  var deltaDeg = isExpanded ? -90 : 90;
  return new Effect.Parallel(
    [
      new Effect.Rotate(img, deltaDeg, { sync: true }),
      new Effect[isExpanded ? 'SlideUp' : 'SlideDown']('related', { sync: true })
    ],
    { duration: 0.3 }
  );
}

document.observe("dom:loaded", function() {
  $('heading').observe("click", toggleExpansion);
});

The only tricky part here is the call to Effect.Parallel. We want the two effects to be on the exact same schedule.

And we’re done. That wasn’t much work at all. Here’s the demo page.

Some might say this isn’t worth doing just to cater to WebKit users. They are, after all, a sliver in the overall browser pie chart. But isn’t this what we mean by “progressive enhancement”? Everyone gets the content, but the browsers we’re especially fond of get the bells and whistles?