CSS3

HTML5, CSS3, and the Bookmarklet that Shook the Web

On Valentine’s Day last year we released a bookmarklet that went viral riding the popularity of the Harlem Shake meme. On the anniversary of its release we’d like to take a moment look back at the technical nuts and bolts of the bookmarklet as a case study in applying HTML5. In fact, the HTML, JavaScript, and CSS we used wouldn’t have worked on a single browser a few years ago. What follows is a technical discussion on how we took advantage of recent browser developments to shake up the web.

Background

Last year the Harlem Shake meme forced itself on to nearly every screen under the sun and, like everyone else, we had joked about doing our office version of the video. After tossing around a few bad video ideas, Ishan half-jokingly suggested a bookmarklet that made a web page do the Harlem Shake. Omar and Hari immediately jumped on the ingenuity of his idea and built a prototype within an hour that had the entire office LOLing. After pulling a classic all nighter we released it on February 14th, declaring “Happy Valentine’s Day, Internet! Behold, the Harlem Shake Bookmarklet”.

Pretty soon it was picked up by news outlets like TechCrunch and HuffingtonPost, and our traffic skyrocketed. Meanwhile the bookmarklet offered a new avenue of expression in the watch-then-remix cycle that is the lifeblood of a viral meme like the Harlem Shake. Instead of creating a video of people dancing, developers could now remix this symbiotic meme in code. Startups like PivotDesk incorporated the bookmarklet into their websites, and HSMaker used the code to build a Harlem-Shake-As-A-Service website. Eventually, YouTube even built their own version as an easter egg on their site.

So, how does it work?

Once you click the Harlem Shake bookmark, a snippet of JS is evaluated on the webpage, just as you’d see by entering javascript:alert(“Hi MozHacks!”); in your address bar. This JavaScript will play the Harlem Shake audio, “shake” DOM nodes (according to timing events attached to the audio), and remove all DOM changes afterward.

How did we attach the audio to the page and get the timing for the shakes just right?

HTML5’s extensive audio support made this implementation fairly easy. All that was required was inserting an <audio> tag with the src pointed to the Harlem_Shake.ogg file. Once inserted into the DOM, the file would begin downloading, and playback begins once enough of the file has been buffered.

HTML5 timed audio events allow us to know exactly when playback begins, updates, and ends. We attach a listener to the audio node which evaluates some JS once the audio reaches certain time. The first node starts shaking once the song is beyond 0.5s. Then, at 15.5s, we flash the screen and begin shaking all of the nodes. At 28.5s, we slow down the animations, and once the audio has ended, we stop all animations and clean up the DOM.

audioTag.addEventListener("timeupdate", function() {
  var time = audioTag.currentTime,
      nodes = allShakeableNodes,
      len = nodes.length, i;
 
  // song started, start shaking first item
  if(time >= 0.5 && !harlem) {
    harlem = true;
    shakeFirst(firstNode);
  }
 
  // everyone else joins the party
  if(time >= 15.5 && !shake) {
    shake = true;
    stopShakeAll();
    flashScreen();
    for (i = 0; i < len; i++) {
      shakeOther(nodes[i]);
    }
  }
 
  // slow motion at the end
  if(audioTag.currentTime >= 28.4 && !slowmo) {
    slowmo = true;
    shakeSlowAll();
  }
}, true);
 
audioTag.addEventListener("ended", function() {
  stopShakeAll();
  removeAddedFiles();
}, true);

How did we choose which parts of the page to shake?

We wrote a few helpers to calculate the rendered size of a given node, determine whether the node is visible on the page, and whether its size is within some (rather arbitrary) bounds:

var MIN_HEIGHT = 30; // pixels
var MIN_WIDTH = 30;
var MAX_HEIGHT = 350;
var MAX_WIDTH = 350;
 
function size(node) {
  return {
    height: node.offsetHeight,
    width: node.offsetWidth
  };
}
function withinBounds(node) {
  var nodeFrame = size(node);
  return (nodeFrame.height > MIN_HEIGHT &&
          nodeFrame.height < MAX_HEIGHT &&
          nodeFrame.width > MIN_WIDTH &&
          nodeFrame.width < MAX_WIDTH);
}
// only calculate the viewport height and scroll position once
var viewport = viewPortHeight();
var scrollPosition = scrollY();
function isVisible(node) {
  var y = posY(node);
  return (y >= scrollPosition && y <= (viewport + scrollPosition));
}

We got a lot of questions about how the bookmarklet was uncannily good at iniating the shake on logos and salient parts of the page. It turns out this was the luck of using very simple heuristics. All nodes are collected (via document.getElementsByTagName(“*”)) and we loop over them twice:

  1. On the first iteration, we stop once we find a single node that is within the bounds and visible on the page. We then start playing the audio with just this node shaking. Since elements are searched in the order they appear in the DOM (~ the order on the page), the logo is selected with surprising consistency.
  2. After inserting the audio, we have ~15 seconds to loop through all nodes to identify all shakeable nodes. These nodes get stored in an array, so that once the time comes, we can shake them.
// get first shakeable node
var allNodes = document.getElementsByTagName("*"), len = allNodes.length, i, thisNode;
var firstNode = null;
for (i = 0; i < len; i++) {
  thisNode = allNodes[i];
  if (withinBounds(thisNode)) {
    if(isVisible(thisNode)) {
      firstNode = thisNode;
      break;
    }
  }
}
 
if (thisNode === null) {
  console.warn("Could not find a node of the right size. Please try a different page.");
  return;
}
 
addCSS();
 
playSong();
 
var allShakeableNodes = [];
 
// get all shakeable nodes
for (i = 0; i < len; i++) {
  thisNode = allNodes[i];
  if (withinBounds(thisNode)) {
    allShakeableNodes.push(thisNode);
  }
}

How did we make the shake animations not lame?

We utilized and tweaked Animate.css’s library to speed up the process, its light and easy to use with great results.

First, all selected nodes gets a base class ‘harlem_shake_me’ that defines animation parameters for duration and how it should apply the styles.

.mw-harlem_shake_me {
  -webkit-animation-duration: .4s;
     -moz-animation-duration: .4s;
       -o-animation-duration: .4s;
          animation-duration: .4s;
  -webkit-animation-fill-mode: both;
     -moz-animation-fill-mode: both;
       -o-animation-fill-mode: both;
          animation-fill-mode: both;
}

The second set of classes that defines the animation’s behavior are randomly picked and assigned to various nodes.

@-webkit-keyframes swing {
  20%, 40%, 60%, 80%, 100% { -webkit-transform-origin: top center; }
  20% { -webkit-transform: rotate(15deg); } 
  40% { -webkit-transform: rotate(-10deg); }
  60% { -webkit-transform: rotate(5deg); }  
  80% { -webkit-transform: rotate(-5deg); } 
  100% { -webkit-transform: rotate(0deg); }
}
 
@-moz-keyframes swing {
  20% { -moz-transform: rotate(15deg); }  
  40% { -moz-transform: rotate(-10deg); }
  60% { -moz-transform: rotate(5deg); } 
  80% { -moz-transform: rotate(-5deg); }  
  100% { -moz-transform: rotate(0deg); }
}
 
@-o-keyframes swing {
  20% { -o-transform: rotate(15deg); }  
  40% { -o-transform: rotate(-10deg); }
  60% { -o-transform: rotate(5deg); } 
  80% { -o-transform: rotate(-5deg); }  
  100% { -o-transform: rotate(0deg); }
}
 
@keyframes swing {
  20% { transform: rotate(15deg); } 
  40% { transform: rotate(-10deg); }
  60% { transform: rotate(5deg); }  
  80% { transform: rotate(-5deg); } 
  100% { transform: rotate(0deg); }
}
 
.swing, .im_drunk {
  -webkit-transform-origin: top center;
  -moz-transform-origin: top center;
  -o-transform-origin: top center;
  transform-origin: top center;
  -webkit-animation-name: swing;
  -moz-animation-name: swing;
  -o-animation-name: swing;
  animation-name: swing;
}

Shake it like a polaroid picture

What started a joke ended up turning into its own mini-phenomenon. The world has moved on from the Harlem Shake meme but the bookmarklet is still inspiring developers to get creative with HTML5.

If you want to see the full source code or have suggestions, feel free to contribute to the Github repo!

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)

Application Layout with CSS3 Flexible Box Module

It has become very easy to create fluid application layouts thanks to the CSS3 Flexible Box Layout Module. In this article we are going to implement a simple application layout, which fills the whole screen, resizes with the browser window and comes with the additional bonus of a draggable splitter.

Instead of the classic <div> elements let’s also use some HTML5 structural tags. This will not only make the code more semantic, but also more convenient to work with, since we can directly address the elements with a CSS type selector without having to rely on id attributes or parent-child relationships.

Take a look at the complete demo to see how it works.

First step: Add Vertical Boxes

We start with only three tags (<header>, <main> and <footer>) in the body.

<!DOCTYPE html>
<html>
<head>
    <title>CSS3 Application Layout</title>
</head>
 
<body>
<header></header>
<main></main>
<footer></footer>
</body>
</html>

Let’s add the CSS to make these three elements fill the space vertically. This is achieved by setting the CSS display property of the <body> to flex and the flex-direction property to column. This tells the browser to lay out the body’s children (<header>, <main> and <footer>) as vertical flexible boxes.

How this available space is distributed can be controlled with the flex shorthand property. You can read about it on MDN. In this application layout though, we don’t want the size to shrink or expand proportionally. Instead the <header> and the <footer> element should have a fixed height, whereas the <main> should just fill the remaining space by setting its flex property to auto.

html, body {
    height: 100%;
    width: 100%;
    padding: 0;
    margin: 0;
}
 
body {
    display: flex;
    flex-direction: column;
}
 
header {
    height: 75px;
}
 
main {
    flex: auto;
}
 
footer {
    height: 25px;
}

Show demo

Second Step: Horizontal Boxes

Let’s add three more elements (<nav>, <article> and <aside>) inside the <main> element. But this time we want them to fill the space inside the <main> element horizontally instead of vertically.

<body>
<header></header>
<main>
    <nav></nav>
    <article></article>
    <aside></aside>
</main>
<footer></footer>
</body>

This is achieved by setting the display property of the <main> element also to flex, but the flex-direction property to row (this is the default). The <nav> and <aside> element should have a fixed width, while the <article> should just fill the remaining space: this is achieved in the same kind of manner as before:

main {
    display: flex;
    flex-direction: row;
    flex: auto;
}
 
nav {
    width: 150px;
}
 
article {
    flex: auto;
}
 
aside {
    width: 50px;
}

Show demo

Thats all. Resize your browser window and enjoy the flexible application layout.

Next step: CSS Refinements

But wait. When there is a lot of content, an element can become smaller than specified and also scrollbars can appear.

Therefore, we need to add a min-width property to all elements where we added a width property. We also should set the overflow property of the <body> and the <main> element to hidden as well as the overflow-y of <article> and <aside> to auto to only show scrollbars where we want them.

body {
	overflow: hidden;
	display: flex;
	flex-direction: column;
}
 
header {
	height: 75px;
	min-height: 75px;
}
 
footer {
	height: 25px;
	min-height: 25px;
}
 
main {
	display: flex;
	flex-direction: row;
	flex: auto;
	border: solid grey;
	border-width: 1px 0;
	overflow: hidden;
}
 
nav {
	width: 150px;
	min-width: 150px;
}
 
article {
	border: solid grey;
	border-width: 0 0 0 1px;
	flex: auto;
	overflow-x: hidden;
	overflow-y: auto;
}
 
aside {
	width: 50px;
	min-width: 50px;
	overflow-x: hidden;
	overflow-y: auto;
}

Note: This does probably not work in Safari yet. You might be able to get it to work by using the -webkit- prefix.

Final Step: Throw a little JavaScript into the Mix

As the final step, we want the user to be able to resize the <aside> element when dragging it with the mouse. For that we add a <div> element as a splitter, which will serve as the drag handle.

<body>
<header></header>
<main>
    <nav></nav>
    <article></article>
    <div class="splitter"></div>
    <aside></aside>
</main>
<footer></footer>
</body>

We set the width of the handle to 4px and give the cursor attribute a value of col-resize, to show the user that this element can be resized East-West.

.splitter {
    border-left: 1px solid grey;
    width: 4px;
    min-width: 4px;
    cursor: col-resize;
}

All what’s left now is to add a little JavaScript, that enables moving the splitter.

var w = window, d = document, splitter;
 
splitter = {
    lastX: 0,
    leftEl: null,
    rightEl: null,
 
    init: function(handler, leftEl, rightEl) {
        var self = this;
 
        this.leftEl = leftEl;
        this.rightEl = rightEl;
 
        handler.addEventListener('mousedown', function(evt) {
            evt.preventDefault();    /* prevent text selection */
 
            self.lastX = evt.clientX;
 
            w.addEventListener('mousemove', self.drag);
            w.addEventListener('mouseup', self.endDrag);
        });
    },
 
    drag: function(evt) {
        var wL, wR, wDiff = evt.clientX - splitter.lastX;
 
        wL = d.defaultView.getComputedStyle(splitter.leftEl, '').getPropertyValue('width');
        wR = d.defaultView.getComputedStyle(splitter.rightEl, '').getPropertyValue('width');
        wL = parseInt(wL, 10) + wDiff;
        wR = parseInt(wR, 10) - wDiff;
        splitter.leftEl.style.width = wL + 'px';
        splitter.rightEl.style.width = wR + 'px';
 
        splitter.lastX = evt.clientX;
    },
 
    endDrag: function() {
        w.removeEventListener('mousemove', splitter.drag);
        w.removeEventListener('mouseup', splitter.endDrag);
    }
};
 
splitter.init(d.getElementsByClassName('splitter')[0], d.getElementsByTagName('article')[0], d.getElementsByTagName('aside')[0]);

Note: For some reason the resizing doesn’t work in IE11 (, Safari?) or Chrome 31. It seems that it has something to do with the display: flex; property value.

View full post on Mozilla Hacks – the Web developer blog

VN:F [1.9.22_1171]
Rating: 0.0/10 (0 votes cast)
VN:F [1.9.22_1171]
Rating: 0 (from 0 votes)