Appetite for Components
Building my first Vue app for Guns N’ Roses
On Tuesday, we launched a teaser campaign for the now announced Guns N’ Roses’ mega box set, Locked N’ Loaded. The fan experience was a mysterious new web property called GNR.FM which was promoted via posters around the world and on the GNR social properties. The site consisted of a countdown clock, some tough looking skulls, and several different paths a user could take which ultimately led to them sharing the news that “Destruction is Coming.” Fans. Went. Nuts. Search the phrase GNR.FM or hashtag #APPETITEFORDESTRUCTION on any social network to see for yourself.
If that isn’t exciting enough, I also decided to make this project the first I would develop with the JavaScript framework Vue.js. Building with Vue was an absolute dream and I could feel it’s structure making me a better developer every step of the way. For every issue I encountered, the Vue guide and developer community had well documented answers. In order to repay the favor, I’ve decided to take you through some of the components we developed for our experience and explain how Vue and other tech solved each problem. Note that this relates to the full experience available on mobile devices.
Intro
The landing page of our experience helps set the tone of the application so we focused on speed, accessibility, and bold visuals. The first thing users notice is an animation flicking through each of the original five band members refreshed skull illustrations. That’s a pretty gnarly phrase. I knew I would be using these same five skulls in an image generator later so I needed a high quality transparent image with a low file size. Impossible? No! I created a single sprite sheet of all five skulls and used TinyPNG to make it as tiny as possible without losing noticeable quality. I then used the steps() function in CSS to create an infinite loop that would flick through each skull animation at a furious but orderly pace.
.skulls{
animation: switch 0.5s steps(5, end) infinite;
background-size: cover
}@keyframes switch {
from {
background-position: 0 0;
}
to {
background-position: -500% 0;
}
}
Right underneath the skulls is the phrase “Destruction is Coming” or perhaps it says something different for you? That’s because Vue’s vue-i18n plugin makes it trivial to add multiple languages to your application globally or on a component level. Given the global appeal of Guns N’ Roses, we thought it was very important to add these translations throughout the experience. Thanks to our teams around the world for providing the proper translations since I only speak English and a little Cajun French.
i18n: {
messages: {
en: {
tagline: "Destruction is coming"
},
fr: {
tagline: "Destruction Vient"
},
es: {
tagline: "Viene Destrucción"
},
it: {
tagline: "Destruction Sta Arrivando"
},
pt: {
tagline: "“Destruction” Está Vindo"
},
de: {
tagline: "Destruction Kommt!"
},
ja: {
tagline: "デストラクションがやってくる。",
}
}
}
The landing page also includes a very simple <countdown>
component. Now I usually use something like jQuery Countdown to accomplish this but I was trying to avoid using jQuery altogether on this project. Instead, I opted for a simple JavaScript interval combined with Moment.js and the plugin Moment Duration Format. Using Moment was a pretty lazy move on my part but I simply did not feel like writing code to pad zeros on the countdown. Incredibly, this is the entire Vue component file:
<template>
<time>{{ remaining }}</time>
</template><script>
import moment from 'moment'
import momentDurationFormat from 'moment-duration-format'export default{
data() {
return {
time: moment(),
end: moment(new Date(Date.UTC(2018, 4, 4, 3, 0, 0))),
interval: null
}
},
computed: {
remaining() {
return moment.duration(
this.end.diff(this.time)
).format("dd hh mm ss")
}
},
mounted() {
this.interval = setInterval(() => {
this.time = moment() if (moment().isAfter(this.end)) {
window.location = "https://www.gunsnroses.com"
}
}, 1000)
},
beforeDestroy() {
clearInterval(this.interval)
}
}
</script>
Clicking the Share Location button, prompts the user to share their current location using the geolocation Web API. Since I planned on using that location in several other components, I needed a way to store the user’s coordinates for future use. Vuex to the rescue. Vuex is Vue’s answer to state management and storing data is as easy as setting up a few mutations and committing the changes when required.
this.$store.commit("updatePosition", result)
Info
Once the user’s position is found, the application immediately redirects to another component which helps lead the user to their next action. This redirection is powered by the vue router plugin which associates Vue components with url routes. In addition to the Intro and this Info component, our application includes the routed components Extend, Compass, Camera, and Share. Once your router structure is configured, routing from one component to another programmatically is as easy calling this single method.
this.$router.push("info")
Users probably aren’t marveling in the fact that the application redirected given the fact that they’re now hearing a stream of an unreleased version of “Shadow of Your Love.” This audio is being played from a single element component which I’ve mapped to a Howler.js sound instance. However, this is a sub component to the main view and we need a way to signal the player component that it’s time to load and play the track. We can accomplish this by creating a simple Global Event Bus. My new favorite website Alligator.io has an excellent tutorial on the subject.
# new bus componentimport Vue from 'vue'
export const Bus = new Vue()# on the player componentBus.$on('tune-in', () => {
this.sound.play()
})# from the info componentBus.$emit('tune-in')
Redirections and rock songs aside, the core function of the Info component is to inform the user of what their next possible action is. All roads lead to sharing. Here are the three possible paths:
- Share — If the user’s position was not found, we would simply encourage them to share the experience with fans.
- Extend — If the user’s location was found but they were not near one of the posters we placed in the real world, we encouraged them to “extend the signal” which is just a fancy way of saying share with customization.
- Explore — If the user’s position was found and they were within 10000 meters of one of our posters, we would suggest that they travel to that location to see one of our real-world teasers up close and take a photo.
Here’s how we can use Vue’s conditional rendering to decide which of these three paths should be presented to the user.
<template v-if="this.$store.state.user">
<template v-if="this.$store.getters.distance < 10000">
<!-- Position found and near poster -->
</template>
<template v-else>
<!-- Position found and not near poster -->
</template>
</template>
<template v-else>
<!-- Position not found -->
</template>
Sweet. Now let’s take a look at the second of these paths, Extend.
Extend
The Extend component is a little personalized social image generator that mashes together a map of your location with one of the skulls. I wanted the UI to be immediate, fun, and simple so I decided to allow users to swipe through the skulls and simply click Extend to create their share image. In order to pull off the snappy swipe functionality, I avoided JavaScript (for my sanity) and instead leaned towards the CSS Scroll Snap Points spec. It took a couple of Codepens to get the functionality I wanted, but the outcome shows that this spec definitely gets the job done with a couple of lines of CSS.
.skulls{
scroll-snap-points-x: repeat(100%);
scroll-snap-type: mandatory;
scroll-snap-destination: 0% 100%;
}.skull{
scroll-snap-align: start;
}
Underneath those skulls is a dark map of the user’s current location powered by the Mapbox Static Map API. Anyone who follows my work will know that this same API was crucial in the creation of the past Marilyn Manson campaign. Like usual, the Mapbox team was excited and supportive when I let them know what we were up to. With the user’s skull selection and map image available, all we need is a bit of HTML5 canvas compositing to generate our share image.
context.drawImage(map, 0, 0)
context.drawImage(skulls, 0, 0)
context.drawImage(attribution, 0, 0)
Let’s pretend for a second that the user refreshed this page or simply decided to come directly to it by navigating to the /extend path. Without a user position in our state, the view would fail to provide a map image. Instead of checking for this error, the Vue router provides Navigation Guard functionality to check for this required information before routing to the component. In this case we would send the positionless user back to the Intro.
{
path: '/extend',
component: Extend,
beforeEnter: (to, from, next) => {
if (store.state.user) {
next()
} else {
next("/")
}
}
}
Most of our users went through the Extend path and while some people did share their map and skull mashups, the majority of users simply clicked the social share buttons that were also provided. Either works fine. 👍🏻
Compass
We put up A LOT of posters around the world and we wanted to help our fans locate those posters to encourage additional social media creation.
For those users who did share their location and were close to one of the posters, we redirected them to a Compass component. In general, this worked very similar the compass I created for my recent Lord Huron Follow The Emerald Star campaign but we didn’t need to worry about streaming audio when user’s reached the location because we had already made audio available. This slimmed the code down quite a bit and really showed off the power of Vue’s single file components.
As an example, if this component only consisted of the rotating dial for the user’s device heading, you could write it this way:
<template>
<div class="dial" :style="{ transform: `rotate(-${heading}deg)` }"></div>
</template><script>
export default {
name: 'Compass',
data() {
return {
heading: 0
}
},
methods: {
updateHeading(e) {
this.heading = e.webkitCompassHeading
}
},
mounted() {
window.addEventListener('deviceorientation', this.updateHeading)
},
beforeDestroy() {
window.removeEventListener('deviceorientation', this.updateHeading)
}
}
</script>
Camera
Once users arrived at one of the posters, they were encouraged to take a photo of the poster. To sweeten the interaction, I decided to build camera functionality directly into the app. I wanted the video element to be a cropped square and was able to achieve this in a single line of CSS code using it’s object-fit property.
video{
object-fit: cover;
}
Using a combination of getUserMedia and HTML5 canvas, we can very simply take snapshots from a video element and composite them with an overlay graphic.
context.drawImage(video, 0, 0)
context.drawImage(overlay, 0, 0)
One of the things that caught my eye about Vue early on was it’s instance lifecycle. This is very important for our Camera component because you’ll want to stop the stream once the user takes a photo. You can do this elegantly by stopping the stream before the component is destroyed.
beforeDestroy() {
stream.getVideoTracks()[0].stop()
}
Share
All paths led to the Share component which consisted of an image which the user was instructed to save and share. If the user took a photo or created their extend image earlier, we would have a HTML5 canvas element on our store which we could render on the page as an image using toDataURL. If not, we would simply fall back to a nice GIF. Here’s what that component looks like:
<template>
<section id="share">
<img :src="share">
</section>
</template><script>
export default{
name: 'Share',
data() {
return {
share: '/static/images/social.gif'
}
},
created() {
if (this.$store.state.share) {
this.share = this.$store.state.share.toDataURL("image/jpeg")
}
}
}
</script>
Thanks
What can I say? I’ve been a fan of GNR ever since I saw the November Rain video on MTV. I had a small freak out when there was a possibility I might be able to help on this campaign. Thanks to Jason, Harold, Kristen and the entire UMe team for the opportunity to play a small part in this massive release. When Jason originally broke down the box set for me over a call, I thought he was pulling my leg. It really is the box set to end all box sets and makes an online stream seem pathetic.
Locked N’ Loaded is available June 29th.