DEV Community

Cover image for Tattooing The Black Keys Fans Using MediaDevices and PixiJS
Lee Martin
Lee Martin

Posted on • Originally published at leemartin.com

Tattooing The Black Keys Fans Using MediaDevices and PixiJS

One of the classic ways a fan can declare their loyalty for a band is by getting a tattoo of the band’s logo inked somewhere on their body. Whether that’s the Foo Fighters “FF,” the Slipknot tribal “S,” or yes, the Black Eyed Peas “?.” In fact, I have a sneaking suspicion that some of you reading this blog have some music related ink on you.

The aesthetic of The Black Keys new release, No Rain No Flowers, is based around the world of American traditional tattoos. I’m talking roses, hearts, scrolls, and sailor jerry inspired typography. As an interactive extra to the album and tour announcement, we’ve developed a new web app that allows fans to virtually tattoo themselves to show their allegiance and then encourage them to join the band's fan club, The Lonely Boys & Girls Club. I researched so many ways to accomplish this (AR, skin segmentation, marker tracking, etc) but landed on something simple and functional: 2D sticker tech. Using either your front or back camera, you can position, rotate, and scale the tattoo on your body and snap a photo of the result. We use a bit of blending and opacity to allow it to bleed into your skin. It’s certainly easier to give someone else a tattoo than give yourself one but I think that’s true to form.

Are you ready to be inked? Join the club today.

tattoo.theblackkeys.com

Read on to find out how this activation was developed using PixiJS.

Development

App Demo

For this application, we gain access to the user’s camera using MediaDevices and then place it onto a PixiJS canvas as a video sprite. Then, we load the tattoo as an additional sprite and give it a bit of opacity and blending to bleed it into the user’s skin. Simple controls are added to allow the user to rotate, scale, and position the tattoo for the perfect inking.

Accessing Camera

I’ve written about accessing a user’s camera using the getUserMedia method of MediaDevices many times before. Not much has changed with this API implementation over time but I have personally started using a custom Typescript written Vue composable to integrate this technology into the experiences I'm building. I’ve made this app’s implementation available as a gist here. As long as I have a <video> tag with the id “cameraVideo” somewhere in my dom, all I need to do is call the start() method to gain access to the user’s camera. I wrap this method in a promise and fire a resolve only when the onloadedmetadata event is complete, signaling the camera stream is ready for action. I’ve also gone ahead and caught the primary errors and have provided real sentences I can display for my users. For this particular app, I’ve added the flip() method to flip the user’s camera from back to front. I also track which way the camera is facing using useState so my app can react appropriately (usually involves mirroring the camera image horizontally when it is facing the user.)

Pixi Setup

One tip from the start is that since we’re going to be capturing the Pixi canvas as an image later, we’ll need to preserve the drawing buffer when initializing the Pixi app. You can do this by adding the appropriate setting.

// Pixi app
const app = new Application()

// Init
app.init({
  canvas: document.getElementById(pixiCanvas),
  height: 1920,
  preserveDrawingBuffer: true,
  width: 1080
})
Enter fullscreen mode Exit fullscreen mode

Since our camera stream is being displayed in a <video> element on our page, we can easily add it as a sprite on Pixi by using the from() method.

// Sprite from video
const videoSprite = Sprite.from(videoEl)
Enter fullscreen mode Exit fullscreen mode

Sometimes you’re dealing with a canvas size that is different from the camera stream size. To handle this I use a little utility library called intrinsic-scale to calculate the dimensions and position which would resize the camera in a way that “covers” the canvas. It’s extremely helpful when I’m building these camera apps.

// Calculate covering
const { width, height, x, y } = resizeToFit('cover', { width: video.videoWidth, height: video.videoHeight }, { width: canvas.width, height: canvas.height })

// Size
videoSprite.height = height
videoSprite.width = width

// Position
videoSprite.x = x
videoSprite.y = y
Enter fullscreen mode Exit fullscreen mode

For the tattoo, we just use the Assets load method of PixiJS to load the image texture and then create a new tattoo sprite using it. In order to blend the tattoo into the user’s skin, we can lower the opacity slightly and also apply a multiply blend.

// Load tattoo texture
const tattooTexture = await Assets.load(tattoo.jpg)

// Create tattoo sprite
const tattooSprite = new Sprite(tattooTexture)

// Apply multiply blend
tattooSprite.blendMode = multiply

// Adjust opacity
tattooSprite.alpha = 0.8
Enter fullscreen mode Exit fullscreen mode

The user is given control to reposition, rotate, and scale the sprite. PixiJS has a great example regarding dragging here. For rotation and scaling, I decided to wire up a HTML range slider using Vue reactivity. When the user changes the value via the slider, we can adjust the property on the tattoo sprite. Here’s the HTML:

<input type=”range” min=”0” max=”2” v-model=”scale” />
Enter fullscreen mode Exit fullscreen mode

And the Javascript:

// Reactive
const scale = ref(1.0)

// Watch scale
watch(scale, (newVal, oldVal) => {
  tattooSprite.scale.set(newVal)
}
Enter fullscreen mode Exit fullscreen mode

Originally, I used HammerJS to allow the user to use their fingers to do this but I found it rather difficult to use two fingers and try to capture a photo of myself. It’s a bit easier to simply hold my phone firmly and use the sliders to adjust.

Inking Tattoo

Tattoo example

In order to ink the user aka take the photo, we simply need to use the toDataURL() and/or toBlob() method of HTML canvas. I use toDataURL to capture a preview url which I can display back to the user in an <img> tag on the outro page.

// Canvas to data url
const url = pixiCanvas.toDataURL(image/jpeg)
Enter fullscreen mode Exit fullscreen mode
<img :src=”url” />
Enter fullscreen mode Exit fullscreen mode

The blob is used to provide the user with either a download or we can use it alongside the Web Share API to prompt the user to share the image via one of their installed apps.

// Canvas to blob
const blob = await new Promise(resolve => pixiCanvas.toBlob(resolve, image/jpeg))

// File from blob
const file = new File([blob], tattoo.jpg, {
  type: blob.type
})

// Web share
navigator.share({
  files: [file]
})
Enter fullscreen mode Exit fullscreen mode

Acknowledgements

Side story here. I always wanted to get my father’s American traditional style tattoo of a rose and scroll adorned with the timeless phrase “Mom.” Or as he calls it, “The worst decision of my life.” 😅 However, my older sister, Maria, beat me to it! However, I still plan on joining the The Loving Sons & Daughters Club soon.

Special thanks to Amber Nagle, Nina Schollnick, and David Adcock for dragging me to the tattoo shop with them. Be on the lookout for new tattoo design options at The Lonely Boys & Girls Tattoo Shop soon.

Top comments (0)