100% Chance of Social


I’ve always been fascinated by weather, and one million websites ago I thought about becoming a meteorologist. That’s probably because I grew up in south Louisiana where we have a “season” for destructive storms. I remember riding out Hurricane Andrew on a house boat with my family. The reason? Well, if the water rose, we would to. We survived the storm and the dream of replacing Jim Cantore waned.

Fast forward to this month and one of my favorite clients, Jimmy Eat World, is releasing their new record Damage, with an umbrella gracing the Morning Breath designed album artwork. No doubt a nod to the aspirations of my youth. Anyway, they needed a way to premiere “Damage” in an accessibly fun way that played on their social strengths. What’s important here is not to satisfy their current fans. The music should do that. Instead we’re trying to trick non fans into participating in an experience that just so happens to be playing Jimmy Eat World music.

I don’t usually take artwork so literally when coming up with ideas, but fuck it, let’s make it rain. Let’s build an experience that translates social activity into a storm and brings out everyone’s inner meteorologist.

Precipitation #

Getting Rain Activity #

The social activity we’ll be polling is hashtags from Twitter and we can do that by creating a simple twitter.rb worker that utilizes the excellent TweetStream gem to listen to Twitter’s Streaming API and using Pusher we can send these updates to the client.

We begin with configuring Pusher and TweetStream by supplying the appopriate API credentials:

# Configure Pusher
Pusher.app_id = ""
Pusher.key    = ""
Pusher.secret = ""

# Configure TweetStream
TweetStream.configure do |config|
  config.consumer_key       = ""
  config.consumer_secret    = ""
  config.oauth_token        = ""
  config.oauth_token_secret = ""
  config.auth_method        = :oauth

Then we create our TweetStream tracking event which gets notified everytime someone tweets the hashtag #damage. This status is then stored in a local database and pushed to the client for rain drop generation.

# Listen for incoming tweets
TweetStream::Client.new.track("#damage") do |status, client|
  tweet = Tweet.from_twitter(status)
  Pusher['damage'].trigger('new_tweet', tweet.to_json(methods: [:track])

Our tweet.rb and track.rb models also include some basic associations that allow us to create a simple incrementer to count the amount of tweets accumulated for each track. We can compare this count to the track’s threshold to figure out if it was unlocked.

class Track
  include DataMapper::Resource

  property :tweets_count, Integer, default: 0
  property :threshold, Integer, default: 1000

  has n, :tweets

class Tweet
  include DataMapper::Resource

  belongs_to :track

  after :create, :increment_counter_cache

  def increment_counter_cache
    if self.track.reload.tweets_count >= self.track.reload.threshold
      self.track.update(state: "unlocked")
      self.track.update(tweets_count: self.track.reload.tweets_count + 1)

In order to test TweetStream on our local machine, I simply create a Rakefile

require 'rubygems'
require 'bundler/setup'

namespace :jobs do
  desc "Heroku worker"

  task :work do
    exec('ruby ./twitter.rb run')

and run:

$ rake jobs:work

Our server, Heroku, requires a Procfile

worker: env TERM_CHILD=1 QUEUES=* rake jobs:work

and scaling a single worker process:

$ heroku ps scale worker=1

Creating Rain Drops #

I used CSS3 to create the drops on Call Drops, but for this I wanted to use Canvas to maximize performance and compatibility. I found the excellent HTML5 JavaScript framework Kinetic.JS that makes simple animation tweening a breeze.

Let’s start by setting up our stage and the main layer our graphics will live.

WIDTH = $(window).width()
HEIGHT = $(window).height()

stage = new Kinetic.Stage
  container: 'rain'

layer = new Kinetic.Layer()
stage.add layer

Rather than learn the ends and outs of Bezier Curves, I provided a simple png sprite image of the rain drop so I could get the exact shape I wanted.

dropImg = new Image()
dropImg = "/images/drop.png"

The drop itself consists of a Kinetic.JS group of two elements, drop and ripple, which are generated right above the page. The drop is created from our preloaded drop image above, and the ripple is created from a new Kinetic.Circle which is scaled down, stroked, and hidden initially. In addition to the drop and ripple, you can customize the elements by using the provided Twitter status varibales.

makeDrop = (status) ->
  # Create group to hold all elements
  group = new Kinetic.Group
   x: Math.random() * WIDTH + 1
   y: 0

  # Create the drop from image
  drop = new Kinetic.Image
    x      : -7
    y      : -25
    image  : dropImg
    width  : 14
    height : 25
  group.add drop

  # Create ripple base
  ripple = new Kinetic.Circle
    y                  : -10
    radius             : 0
    scaleX             : 5
    stroke             : 'white'
    strokeScaleEnabled : false 
    strokeWidth        : 4
    visible            : false
  group.add ripple

The animation itself consists of two tweens:

  1. Fall - This animates the drop falling from the top of the page to bottom. Once the animation completes, we hide the drop and show the ripple.
  2. Splash - Once the ripple is shown, we grow the radius while also fading it out with easing out and in to simulate a delicate water ripple.

Let’s take a look at that series of animations.

# Animate the fall and splash with series of callbacks
fall = new Kinetic.Tween
  node     : group
  duration : 3
  y        : HEIGHT - 100
  onFinish: ->

    slash = new Kinetic.Tween
      node     : ripple
      duration : 3
      radius   : 20
      opacity  : 0
      easing   : Kinetic.Easing.EaseOutIn
      onFinish: ->

We then listen for those Pusher updates from the server and create a new rain drop each time a new status is tweeted.

pusher  = new Pusher ''
channel = pusher.subscribe 'damage'

channel.bind 'new_tweet', (status) ->
  flash() if status.nickname is "jimmyeatworld"

But what if Jimmy Eat World tweets? Make some lightning using Howler.JS of course.

lightning = new Howl(urls: ["/lightning.mp3"])

flash = ->
  $("#flash").fadeIn 0, () -> $("#flash").fadeOut(1000)

Rain Gauge #

In addition to making sure it played audio and scaled responsively, I wanted to do something special with the mobile interface, so I decided to use the same real-time unlocking data to create a mobile udometer to compliment the desktop experience. The idea being that you could watch the drops falling on your desktop screen while gauging the unlock levels on your mobile device.

Damage Mobile

It was a fun experiment in dual interactive design and something I will no doubt revisit in the future.

Post Storm #

Thanks for reading. I hope this sheds a bit of sunlight on what it takes to pull off a campaign like this. The power truly lies in all of the excellent libraries I used, so special thanks goes out to those authors. Follow me on Twitter for the latest and please let me know if you have any questions or comments.

Finally, don’t forget to pick up Jimmy Eat World’s eight studio album, Damage, in stores everywhere tomorrow!


Now read this

De-veloped in The Comatorium

An origin story about forums, donations, and death. At the end of my senior year at South Terrebonne High School in 2003, I was inspired by Mr. Boudreax’s Programming 101 class to develop a website. It just so happened that my buddy... Continue →