How to create a scratch card effect with vanilla JavaScript

Let’s begin by taking a look at what we’re creating. By the end of this tutorial, you will have something like this (move your mouse cursor over the scratch panel and watch a uniquely generated code reveal itself):

OverView

In HTML, the <canvas> element is a powerful tool for directly rendering 2D graphics, such as images, shapes, and text, in the web browser. It provides a drawing surface that can be manipulated through JavaScript, allowing us to create dynamic and interactive graphics. We’re going to rely on the <canvas> elements in this tutorial.

We’ll start by creating a card element displaying a random code. Next, we’ll create a canvas with the exact dimensions of the card element. This canvas will be overlaid on top of the card element, hiding the number beneath it.

To simulate the scratch-off effect, we’ll use the globalCompositeOperation property of the canvas context. By setting ctx.globalCompositeOperation = "destination-out," parts of the canvas will be erased when the user moves the mouse over the canvas. The code on the card will gradually be revealed as the user continuously scratches (moves the mouse) over the canvas.

HTML Structure

The HTML Structure using Bootstrap will look like this:

1
<div class="d-flex flex-column align-items-center justify-content-center vh-100">
2
<h1 class="text-center mb-5">Scratch Below</h1>
3
  
4
  <i class="fas fa-arrow-down fa-6x mb-4"></i>
5
  <div class="row mt-5">
6
    <div class="col-md-4 col-sm-6 col-8">
7
      <div id="card" class="mx-auto border position-relative bg-white">
8
        <div id="code" class="text-center"></div>
9
        <canvas
10
          class="position-absolute top-0 start-0"
11
          id="scratch-pad"
12
        ></canvas>
13
      </div>
14
    </div>
15
  </div>
16
</div>

The <canvas> element is positioned on top of the card element using the position-absolute class along with top-0 and start-0. These styles ensure that it starts at the top-left corner of the parent element.

Add the Bootstrap CDN link in your HTML document’s <head> section. 

1
<link
2
  href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"
3
  rel="stylesheet"
4
  integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC"
5
  crossorigin="anonymous"
6
/>

Custom CSS Styles

Add a custom font and a background color to the body of the page.

1
@import url("https://fonts.googleapis.com/css2?family=DM+Mono:ital,wght@0,300;0,400;0,500;1,300;1,400;1,500&display=swap");
2
body {
3
    font-family: "DM Mono", monospace;
4
    background-color: #f3f1f5;
5
}

The card element will have the following dimensions

1
  #card {
2
    width: 400px;
3
    height: 90px;
4

5
  }

Add the following styles to the code element, which will display the random characters.

1
#code {
2
    font-size: 50px;
3
    padding: 20px;
4
    background-color: white;
5
    line-height: 40px;
6
    font-weight: 800;
7
  }

Custom Cursor

In CSS, the cursor property controls the appearance of the mouse pointer when it hovers over an element. While the default value of the cursor is a hand icon, you can also customize the cursor by specifying an image using the url value. This allows you to create a unique scrolling experience. Let’s define our custom cursor.  

1
canvas {
2
cursor: url("https://essykings.github.io/JavaScript/coin42.png") 50 50,
3
  crosshair;
4
}

In our case, we are using this coin (downloaded from one of Envato’s 3D coin renders) as the cursor.

Create Random Characters

To ensure the characters revealed after scratching are unique, we will create a function to generate a random character each time the application loads. Create a function called generateRandomString() and add the code below.

1
function generateRandomString(length) {
2
    const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
3
    let code = "";
4
    for (let i = 0; i < length; i++) {
5
        code += characters.charAt(
6
        Math.floor(Math.random() * characters.length)
7
      );
8
    }
9
    return code;
10
  }

In the code above:

  • const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; defines a variable characters which contains the uppercase alphabet letters (A-Z) and the digits (0-9).
  • let code = "; initializes an empty string to store the final character string.
  • For the for loop,  a random number will be generated and stored in the code variable in each iteration,

Update the text content of the code element to show the generated character.

1
document.getElementById("code").textContent = generateRandomString(12);

The app so far looks like this:

The next step is to draw on the canvas to hide the content on the card element.

Draw Canvas

First, get the canvas and set the width and height to be the same as the card element.

1
const canvas = document.getElementById("scratch-pad");
2
canvas.width = card.offsetWidth;
3
canvas.height = card.offsetHeight;

Next, get the canvas context. A context object is where the drawings will be rendered.

1
const ctx = canvas.getContext("2d");

The next step is to fill the canvas with a custom gradient color to hide the content on the card element. To create the gradient, initialize a gradient instance using the createLinearGradient() method.

1
 const gradient = ctx.createLinearGradient(
2
        0,
3
        0,
4
        canvas.width,
5
        canvas.height
6
      );

The gradient starts from the top-left corner (0, 0) and ends at the bottom-right corner (where the canvas ends).

Now that we have a gradient, we can specify the colors which will appear on the gradient. 

1
gradient.addColorStop(0, "#DFBD69");
2
gradient.addColorStop(1, "#926F34");
3
ctx.fillStyle = gradient;

ctx.fillStyle tells the context what to use when drawing; in this case, a gold brown gradient will be used to fill shapes drawn on the canvas.

Finally, fill the entire canvas with the gradient color by drawing a rectangle on the entire canvas.

1
  ctx.fillRect(0, 0, canvas.width, canvas.height);

So far we have this:

Scratch Off Effect

The last step is to implement the scratch-off effect. Define a variable called isDrawing; this variable will keep track of whether we are drawing on the canvas. Initially the variable will be set to false

Then, we need to set event listeners for mouse movements to capture mouse and touch events. When the user moves the mouse or uses the finger to touch the canvas (in touch screens), the scratching effect will be implemented.

1
canvas.addEventListener("mousemove", (e) => {
2
isDrawing = true;
3

4
});
5

6

7
canvas.addEventListener("touchstart", (e) => {
8
isDrawing = true;
9

10
});
11

12
canvas.addEventListener("touchmove", (e) => {
13

14
}
15
});
16


Here is a summary of the mouse and touch events:

  • mousemove event: during this event, the isDrawing variable is set to true, signalling that drawing should start.
  • touchstart event: during this event the isDrawing variable is set to true, indicating the beginning of a touch-based drawing.
  • touchmove event: We will implement the scratch effect during the touchmove event (i.e., when a user moves their finger on the screen).

Let’s implement each event independently; we will start with the mouse move event. When the use starts moving the mouse, we want the scratch effect to be applied on the canvas.

Create the scratch() function, which will be called when the user starts drawing.

1
function scratch(e) {
2

3
}

In the scratch() function, calculate the position of the mouse or touch event relative to the canvas using getBoundingClientRect(). The getBoundingClientRect() method is useful for obtaining the position of an element relative to the viewport. In our case, we want to get the position of the mouse or touch relative to the canvas.

To get the mouse or touch position relative to the canvas, we subtract the canvas’s position from the event’s coordinates:

1
function scratch(e) {
2
    const rect = canvas.getBoundingClientRect();
3
    const x = e.clientX - rect.left;
4
    const y = e.clientY - rect.top;
5
}

Set the composite operation to destination-out; This means that anything drawn on the canvas will erase what is already on the canvas or make it transparent. When the canvas is erased, the content on the card element will be revealed.

A circle with a radius of 20 pixels will be drawn, effectively creating a “scratch” effect on the canvas. Update the function as follows.

1
function scratch(e) {
2
    const rect = canvas.getBoundingClientRect();
3
    const x = e.clientX - rect.left;
4
    const y = e.clientY - rect.top;
5

6
    ctx.globalCompositeOperation = "destination-out";
7
    ctx.beginPath();
8
    ctx.arc(x, y, 20, 0, Math.PI * 2, false);
9
    ctx.fill();
10
  }

  • ctx.beginPath(); begins a drawing path
  • ctx.arc(x, y, 20, 0, Math.PI * 2, false); draws a circular path (an arc) with a radius of 20 pixels starting from the mouse position
  • ctx.fill() fills the circle, effectively erasing the part of the canvas covered by the circle arc. 

Now, update the mouse event functions. On mouse move, we will set isDrawing to true to signify that the drawing has started and then invoke the scratch() function to start the erasing as the mouse moves.

1
canvas.addEventListener("mousemove", (e) => {
2
isDrawing = true;
3
scratch(e);
4
});

Update the touchstart and touchmove events as follows: as follows.

The scratching effect will start when a touch begins. e.touches[0] refers to the first touch point. As the user moves their fingers across the canvas on touch devices, the scratching effect will continue .

1
canvas.addEventListener("touchstart", (e) => {
2
    isDrawing = true;
3
    scratch(e.touches[0]);
4
  });
5

6
  canvas.addEventListener("touchmove", (e) => {
7
    if (isDrawing) {
8
      scratch(e.touches[0]);
9
    }
10
  });

Conclusion

We have successfully created a realistic scratch-off effect using vanilla JavaScript! This app has showcased how powerful the HTML5 <canvas> element can be in crafting engaging and interactive user experiences. Below is a reminder of the final product.