Topics

Creating the new landing page for See+Do

We’ve been working on See+Do over the past few months, to let us add new cities to the platform. As part of this effort we needed to add a new landing page. I wanted to create something interesting that users would remember and add some interaction to the page. So I decided to let users lob Emojis all over the place.

To do this I used Matter.js and created a simple 2D physics sandbox. Matter.js is a fun library to work with, it’s a powerful 2D physics engine with a fairly easy to use interface, the only issue is the documentation can be a bit painful to get around and a general lack of examples with up to date code.

I want to cover some of the interesting parts of building the landing page.

Populating the world

Different objects are added in different ways, we have a few types of emoji as well as invisible hitboxes being added to replicate the DOM in the canvas. I’ll go over the key points of each.

Emojis.

Emojis are randomly generated and dropped into the world, right now there are 3 different kinds of emoji: Faces, Pictures and Pizza. These different types are specified in an array at the top of the file to make it easier to add more types later on.

var emojiTypes = [
  {
    name: 'Laugh',
    shape: 'circle',
    radius: 38.75,
    sprite: 'http://i.imgur.com/9Nj59hX.png'
  },
  {
    name: 'Picture',
    shape: 'rectangle',
    width: 80,
    height: 65,
    sprite: 'http://i.imgur.com/MAFn8RF.png'
  },
  {
    name: 'Love',
    shape: 'circle',
    radius: 38.75,
    sprite: 'http://i.imgur.com/IOADJCL.png'
  },
  {
    name: 'Pizza',
    shape: 'triangle',
    points: [
      { x: 0, y: 0 }, { x: -40, y: -80 }, { x: -80, y: 0 }
    ],
    sprite: 'http://i.imgur.com/AbJKujs.png'
  }
]

Each type has to be generated differently, picture frames can just be a rectangle (the sprite doesn’t have to be completely accurate to the shape). Circles are all the same size with different sprites and then triangles are simple polygons.

if (emojiTypes[randomNumber].shape === 'circle') {
  var emoji = Bodies.circle(horizontalOffset, verticalOffset, emojiTypes[randomNumber].radius, {
    render: {
      sprite: {
       texture: emojiTypes[randomNumber].sprite
      }
    },
    restitution: 0.3,
    angle: randomAngle
  });
}

if (emojiTypes[randomNumber].shape === 'rectangle') {
  var emoji = Bodies.rectangle(horizontalOffset, verticalOffset, emojiTypes[randomNumber].width, emojiTypes[randomNumber].height, {
    render: {
      sprite: {
        texture: emojiTypes[randomNumber].sprite
      }
    },
    restitution: 0.3,
    angle: randomAngle
  });
}

if (emojiTypes[randomNumber].shape === 'triangle') {
  var emoji = Bodies.fromVertices(horizontalOffset, verticalOffset, emojiTypes[randomNumber].points, {
    render: {
      sprite: {
        texture: emojiTypes[randomNumber].sprite
      }
    },
    restitution: 0.3
  })
}

World.addBody(engine.world, emoji);
emojis.push(emoji);

Generating Platforms

On page load and resize I do a check against the DOM and find any elements with the js-canvas-dom class. Then for each of these I insert a rectangle into the world and set its width/height based on the element. Then reposition them based on the top/left offset of the dom element.

$('.js-canvas-dom').each(function() {
  var width = $(this).width();
  var height = $(this).height();
  var cords = $(this).offset();
  var x = cords.left + width / 2;
  var y = cords.top + height / 2;
  var outline = 'transparent';

  // Make the platform based off the dimension/cords of the DOM element
  var platform = Bodies.rectangle(x, y, width + 25, height, {
    isStatic: true,
    render: {
      lineWidth: 0,
      strokeStyle: outline,
      fillStyle: 'transparent'
    }
  });
  // Keep a track of the platform so we can modify/remove it later.
  platforms.push(platform);
  // Let it free into the world.
  World.addBody(engine.world, platform);
})

The final effect is that it seems like the canvas elements can interact with the elements on the page. So you can bounce the emojis off the city titles.

Limiting objects on screen

Every time we add more emojis to the world, we need to do a check and make sure that we don’t add too many. This is partly due to performance limitation and also just to try and keep visual clutter to a minimum. If it turns out we do have too many then we just remove some of the old ones before adding anymore.

function limitBodies(bodies, limit) {
  if (bodies.length > limit) {
    var diff = bodies.length - limit;
    removeBodies(bodies.splice(0, diff), true);
  }
}

Making a graceful exit

Instead of having the emojis just randomly vanish from existence I implemented a nicer leaving animation. Before we send the bodies through to removeBodies() we order them using .sort(sortPositionY) on the array, so the emojis fall out of the world from the bottom row.

emojis.sort(sortPositionY)

function sortPositionY(a,b) {
    return b.position.y - a.position.y;
}

We then change the collision group so the emojis fall out of the world and then after a short delay, remove them from the world.

function removeBodies(bodies, fall) {
  var fall = typeof fall !== 'undefined' ?  fall : false;

  for (var i = bodies.length - 1; i >= 0; i--) {
    var body = bodies[i];

    if (bodies[i].collisionFilter && fall) {
      // They no longer colide with any other objects
      bodies[i].collisionFilter.mask = 2

      // After 2 seconds they should be out of the viewport.
      setTimeout(function(bodies, i) {
        Matter.Composite.remove(engine.world, bodies[i]);
      }.bind(this, bodies, i), 2000)
    } else {
      Matter.Composite.remove(engine.world, body);
    }
  }
}

This creates a much more natural leaving animation. As you can see below:

If you want to take a look at all the code then its available here on GitHub or if you want to try the live version, you can here.