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.
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 end
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]) end
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 end 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") else self.track.update(tweets_count: self.track.reload.tweets_count + 1) end end end
In order to test TweetStream on our local machine, I simply create a
require 'rubygems' require 'bundler/setup' namespace :jobs do desc "Heroku worker" task :work do exec('ruby ./twitter.rb run') end end
$ rake jobs:work
Our server, Heroku, requires a
worker: env TERM_CHILD=1 QUEUES=* rake jobs:work
and scaling a single worker process:
$ heroku ps scale worker=1
Creating Rain Drops
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
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:
- 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.
- 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: -> drop.destroy() ripple.show() slash = new Kinetic.Tween node : ripple duration : 3 radius : 20 opacity : 0 easing : Kinetic.Easing.EaseOutIn onFinish: -> ripple.destroy() splash.play() fall.play()
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) -> makeRain(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 = -> lightning.play() $("#flash").fadeIn 0, () -> $("#flash").fadeOut(1000)
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.
It was a fun experiment in dual interactive design and something I will no doubt revisit in the future.
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!