Developing a scratch card track reveal for Pop Smoke | by Lee Martin | Jul, 2021


The first thing I tackled was the layout of the scratch card. While I could be inspired by the design of real-world scratch cards, the layout of our digital scratch card had to be based on responsive design principles. That’s because it was going to be interacted with on screens of all sizes. It also helped to know the content which users would be scratching to reveal: track titles. Track titles come in various lengths so a longer horizontal area would work best. A good example of this is the labels you might find in a jukebox. I figured I could fit four horizontal scratch areas and leave room for a scratch card header and footer.

With a layout in mind, I started to consider the functionality. Users should be able to scratch each of the four scratchable areas to reveal a piece of content. Usually that content would simply be a “Try again Woo” message but there was a small chance it would be one of the track titles. At any point, we should know how much of the scratchable area was scratched and in turn be able to decide if a card was indeed scratched. At this point, the user should be able to receive a new card. I knew Vue.js was going to be the perfect solution for this and broke it down into several components.

  • <ScratchContent> — the content to be revealed
  • <ScratchCover> — the scratchable surface which conceals content
  • <ScratchArea> — layout of scratch-content and scratch-cover
  • <ScratchCard> — the card, holds four scratch-areas

Using props, methods, and listeners, each component should be able to inform the other components of the card.

The core experience of this campaign is allowing the user to actually do some scratching so the <ScratchCover> Vue component is probably the most important. This was achieved using HTML <canvas> and the touch gestures library Hammer.js. The component has a single element: the <canvas> tag.

<template>
<div class="scratch-cover">
<canvas ref="canvas"></canvas>
</div>
</template>

Covering

The first thing we’ll do is draw the cover image onto the <canvas> directly from the full card image using the drawImage method. There is a subtle difference between each scratch area cover due to the smoke in the background so this is an organized way of handling things.

let canvas = this.$refs.canvas// set the canvas to match it's parent size
canvas.height = canvas.parentElement.getBoundingClientRect().height
canvas.width = canvas.parentElement.getBoundingClientRect().width
let context = canvas.getContext('2d')// draw the associated section of card image
context.drawImage(
cardImage,
24,
[260, 336, 412, 488][cardIndex],
432,
64,
0,
0,
canvas.width,
canvas.height
)

All of the sizes and positioning used in the drawImage method were just manually pulled from the original card design I put together in Figma.

Scratching

The actual scratching interaction is powered by Hammer.js. As users pan over the <canvas> with their mouse or finger, they are actually erasing that area. In order to keep the erasing interesting, we’ll use a pre-generated eraser image. To erase with the image, we just need to use the ‘destination-out’ globalCompositeOperation. Which is described as, “The existing content is kept where it doesn’t overlap the new shape.” Here’s how it all comes together.

let hammer = new Hammer(canvas)// set pan
hammer.get('pan').set({
direction: Hammer.DIRECTION_ALL
})
hammer.on('panmove', e => {
// set the composite operation
context.globalCompositeOperation = 'destination-out'
// "draw" the eraser image
context.drawImage(
eraserImage,
e.center.x - e.target.getBoundingClientRect().left - 25,
e.center.y - e.target.getBoundingClientRect().top - 25
)
// update scratched progress
this.updateProgress()
// generate fleck
this.$nuxt.$emit('fleck')
})

This is also when we’ll handle calculating how much of the area has been scratched (which we’ll discuss next) and generate animated flecks (which we’ll discuss later.)

Scratch Progress

As the user scratches the cover, we’ll calculate how much they have scratched (between 0.0 and 1.0) and inform other components of the card. We can do this by checking which percentage of the <canvas> consists of transparent pixels and then emit the new progress. In order to get all the pixels, we’ll use the getImageData() method. This will return the red, green, blue, and alpha amounts of each pixel as one long array. Step by 4 in the for loop in order to check each pixel. Then simply divide the counted transparent pixels by the total pixel area of the <canvas> to get the progress.

let counter = 0// get the canvas image data
let imageData = context.getImageData(
0,
0,
canvas.width,
canvas.height
)
// get the total length of image data
let imageDataLength = imageData.data.length
// loop through all pixels and if r, g, b, & a is zero, increment
for (let i = 0; i < imageDataLength; i += 4) {
if (imageData.data[i] === 0 && imageData.data[i+1] === 0 && imageData.data[i+2] === 0 && imageData.data[i+3] === 0) {
counter++
}
}
// progress is counter divided by overall area
let progress = counter >= 1 ? counter / (canvas.width * canvas.height) : 0
// emit the new progress
this.$emit('updateProgress', Math.round(progress * 100) / 100)
Early prototype of the scratch card

There isn’t anything all that spectacular about the <ScratchContent> component. It is either going to be the “Try Again Woo” empty image or possibly one of the track images. This is decided back when the <ScratchCard> component is originally initialized. First, I’ve integrated all of the tracks into the experience using the @nuxt/content module. Then, I have a generateCard() method which establishes what each of the 4 scratch-areas will conceal. There is a 1 in 10 chance the area will conceal one of the tracks, which are selected at random. If not, we’ll simply display the empty image.

this.cards.push({
scratched: false,
areas: Array(4).fill().map((item, i) => {
let content
if (Math.random() < 0.1) {
content = {
type: 'track',
data: tracks[Math.floor(Math.random() * tracks.length)]
}
} else {
content = {
type: 'empty'
}
}
return {
content: content
}
}
})

An earlier version of this app had contesting built in. I added that to our experience here by adding a 3rd possible content type, contest, with even lower odds. We ended up dropping that piece but it could be an awesome tactic for a future campaign using scratch cards. If the user scratched off a contest block, their email address was automatically entered to win.

In addition to laying out the scratch-cover and scratch-content components on top of each other, the <ScratchArea> component listens for the change in progress from the scratch-cover component and decides whether or not an area is scratched if the scratch progress exceeds a specified threshold. In the case of our app, 10%. Here’s what that looks like.

<template>
<div id="scratch-area">
<ScratchContent :content="area.content" />
<ScratchCover @updateProgress="progress = $event" />
<div>
</template>

As the local progress variable changes, we can use a computed property to decide if scratched.

computed: {
scratched() {
return this.progress > 0.1
}
}

And we’ll watch for changes to scratched so when it does flip to true, we can inform the <ScratchCard> parent component.

watch: {
scratched(newVal, oldVal) {
this.$emit('updateScratched', newVal)
}
}

Back on the <ScratchCard> component, we’ll have a similar computed method which checks to see if all areas are scratched, in turn determining if the card itself was scratched.

computed: {
scratched() {
return this.areas.every(area => area.scratched)
}
}

Isn’t Vue.js great?



Source link

Latest articles

Related articles

Leave a reply

Please enter your comment!
Please enter your name here