Crafting Shields for Vikings with Vue Components and Canvas

Featuring Amon Amarth’s latest release Berserker

Lee Martin
Level Up Coding

--

“Bravery is half the victory” - The Saga of Harald Hardrada

I love a good image generator so I was pumped when Amon Amarth asked me to support their latest release, Berserker, by developing a generator based on the incredible art. 🛡 The solution is a simple Nuxt.js app hosted on Netlify which takes four names and renders them as Viking Runes onto a round shield. That shield is then composed on backdrops which follow the suggested dimensions of each major social platform. If, like Amon Amarth, you have spent a lot of energy on the creative direction of your release, it makes sense to want to allow fans to personalize some of the visuals using scalable image generation. Create your shield today and read on to find out about some of the generator’s technical solutions.

I mentioned this in my recent Motown project case study but I’ve begun using a combination of Vue.js components and HTML5 canvas to create dynamic images. My current base setup looks a little something like this.

<template>
<img :src="canvasImage" />
</template>
<script>
export default{
computed: {
canvasImage() {
let canvas = document.createElement('canvas')
canvas.height = 1080
canvas.width = 1080
let context = canvas.getContext('2d') context.fillStyle = 'red'
context.fillRect(0, 0, 1080, 1080)
return canvas.toDataURL('image/jpeg')
}
}
}
</script>

The HTML template is an image tag with a dynamic source which references a computed property called canvasImage. Within this property, I utilize an offline canvas to generate a new image such as a 1080x1080 red square in this case. That canvas is then returned as a data URL so it may be displayed by the <img>. I could then include this component into one of my views and place it like any other image.

The shield itself was constructed out of two layers: the names and the background. Since the names are dynamic, we generate those dynamically using a new canvas and custom typography. Once that layout is fulfilled, it is placed on top of the pre-rendered background image. Both of these layers share a similar problem in that they are dependent on external assets. In order for this component to perform properly across multiple browsers, you must first preload your fonts and images.

For the image, I add in a new promise method called loadImage which takes the url of an image, loads it, and then returns resolution on completion.

loadImage(url) {
return new Promise((resolve, revoke) => {
let img = new Image()
img.crossOrigin = 'Anonymous' img.onload = () => {
resolve(img)
}
img.src = url
})
}

Using custom fonts in canvas can be very tricky but Bram Stein wrote the excellent Font Face Observer loader to help recognize when custom typography was loaded. Back in our CSS, we may have a @font-face declaration which looks something like this.

@font-face{
font-family: 'Runes';
src: url('/runes.woff2') format('woff2'),
url('/runes.woff') format('woff');
font-weight: normal;
font-style: normal;
}

Back in our component, we can then use Font Face Observer to check to see if that typeface has loaded.

let font = new FontFaceObserver('Runes')font.load().then(() => {
console.log('loaded')
})

As you can see from this snippet, Font Face Observer also returns a promise so we can actually combine both our image and typography loading together to be confident that all assets are ready before called canvas.

let font = new FontFaceObserver('Runes')Promise.all([
loadImage('/background.jpg'),
font.load()
]).then(data => {
let background = data[0]
})

In order to generate the names, I just use a few canvas methods to place the text exactly where it needs to go. I’ll reference my design in Figma to find the exact size and positioning.

context.textAlign    = 'center'
context.textBaseline = 'middle'
context.font = '24px Runes'
context.fillText('Lee', 540, 540)

In the case of this project, the text was generated on a separate canvas and then placed onto another canvas which included the background image. This allowed the names to be generated in a very exact manner but then resized and placed onto varied background images. You can use the drawImage method to place both the preloaded background and dynamic names onto a new canvas.

context.drawImage(background, 0, 0)
context.drawImage(names, 135, 135, 810, 810)

The last thing you’ll want to do is provide your user with download instructions which fit their current environment. On a mobile device, the user can’t click to download an image so the instructions should read: “Press and hold to download image.” (I use mobile-detect to see if the user is coming from a mobile device.) On the desktop, I’ll wire up one-click downloading with downloadjs to provide the best experience. You can actually pass in the same computed method to power this function.

download(canvasImage)
Amon Amarth

Thanks to Stephen Reeder and 5bam for bringing me in on this one and congrats to Amon Amarth for releasing their new record Berserker. Stream it today on Spotify or Apple Music. 🤘🏻

--

--

Netmaker. Playing the Internet in your favorite band for two decades. Previously Silva Artist Management, SoundCloud, and Songkick.