Scratch cards are a familiar element of web interaction. To achieve the effect of scraping coating, we need to use canvas to achieve it, which must be clear to every front-end engineer. Scratch card is not difficult to achieve, but it involves a lot of knowledge, master these knowledge points, help us to have a deeper understanding of the principle, for improving the ability to draw inferential conclusions is very helpful. In this issue, the scratch-card implementation is taken as an example to share how to scientifically and reasonably encapsulate functions and explain the relevant knowledge points involved.

Take a look at the final result:

What are the knowledge points involved in implementing scratch cards?

Canvas element size vs. Canvas size

错 句 2: Prototype __proto__ constructor

Canvas’s globalCompositeOperation

The third addEventListener is the passive property

Canvas ImageData

Let’s move on to the official content of this share.

1 Demand Analysis

In order to meet the needs of more scenarios, we provide as many parameters as possible for the convenience of users. Start by thinking about what configuration options a scratchcard might require from a product and UI perspective.

  • Coating style (picture or solid color)
  • Brush radius
  • When you reach the percentage, scrape off the entire coating
  • Effect of scraping off all coating (fade or erase directly)

Next, add technical configuration options:

  • The canvas element
  • Screen pixel display multiple (suitable for Retina display)
  • Fade out the transition animation time

OK, after confirming the above configuration parameters, we can officially start work.

2 Page Construction

The project directory structure is as follows:

| - award. JPG < -- -- scratch CARDS the underlying results page picture | - index. The HTML | - scratch - 2 x. JPG < -- scratch CARDS coating picture | - scratchcard. The jsCopy the code

The page structure is simple, the background of the DIV displays the results, and the canvas inside the div is used for the coating.

Create a new index.html file and add the following code (the HTML template code is skipped) :

HTML code:

<div class="card">
    <canvas id="canvas" width="750" height="280"></canvas>
</div>
Copy the code

The CSS code:

.card {
    width: 375px;
    height: 140px;
    background: url('award.jpg');
    background-size: 375px 140px;
}
.card canvas {
    width: 375px;
    height: 140px;
}
Copy the code

Award.jpg uses a 2x image, so use background-size to scale back to 1 display size.

It can be found that the width and height of canvas in HTML are inconsistent with those of CSS. The reason is to get used to Retina 2X. This involves the knowledge point of canvas canvas size.

The page now looks like this, and the resulting image is displayed:

Canvas element size vs. Canvas size

In HTML, canvas width and height are the size of the canvas, which is generally referred to as the “drawing area size” of the canvas. It must be distinguished from the display size of the element.

Our resulting image material is 750×280, so for the canvas to fully draw this image, the canvas size also needs to be 750×280.

So the element size is the “display size” of the canvas on the page. Set the width and height of the Canvas element through CSS to make it correctly displayed.

3. Build a prototype of the class

New scratchcard. Js.

Based on the requirements analysis in Chapter 1, the prototype of class is as follows:

functionScratchCard(config) {this.config = {// canvas element canvas: null, // 65, // The image layer coverImg: null, // the solid color layer, if the image layer value is not null, the solid color layer is invalid, // All scratch open callbackdoneCallback: null, // erase radius: 20, // screen multiple pixelRatio: 1, // display all fadeOut time (ms) fadeOut: 2000 } Object.assign(this.config, config); }Copy the code

Passing parameters to functions using objects has many advantages:

  1. Parameter semantics, easy to understand
  2. Don’t worry about the order of arguments
  3. The addition, deletion, and order adjustment of parameters do not affect the use of service codes

Assign overrides the default config parameters passed in using the object. assign method. Properties not found in the passed config, the default configuration is used.

Add scratchcard.js in index.html and insert script code at the bottom of the body:

new ScratchCard({
    canvas: document.getElementById('canvas'),
    coverImg: 'scratch-2x.jpg',
    pixelRatio: 2,
    doneCallback: function() {
        console.log('done')}});Copy the code

The scratch card class is very easy to use, passing only values that do not use the default configuration.

4 implement ScratchCard

4.1 Prototype ScratchCard

Continue writing scratchcard.js:

    functionScratchCard(config) { this.config = { ... } object.assign (this.config, config) + this._init();} object.assign (this.config, config) + this._init(); } + ScratchCard. Prototype = {+ constructor: ScratchCard, + //function() {}
+   }
Copy the code

Here’s constructor: ScratchCard, just to be more rigorous, and it’s ok to omit this line.

2. Prototype and constructor from the code.

错 句 2: Prototype __proto__ constructor

Two things to keep in mind:

  1. __proto__andconstructorProperties are unique to objects (functions are also objects).
  2. prototypeAttributes are unique to functions.

※ Since JS functions are also objects, they also have __proto__ and constructor attributes.

【 __proto__ 】

__proto__ attributes all point from one object to one object, that is, to their prototype object (also known as parent object).

If the __proto__ attribute does not exist in the object, then the __proto__ attribute refers to the object (parent object). If the __proto__ attribute does not exist in the parent object, then the __proto__ attribute refers to the object (grandfather object). If not, keep looking until the top of the prototype chain is null. Null is the end of the prototype chain.

A chain of objects linked to null by __proto__ attributes is called a prototype chain.

“Prototype”

The Prototype object is function specific; it points from a function to an object. It means the prototype object of the function, that is, the instance created by the function.

Var demo = new demo ()function Demo(config) { ... }
Copy the code

Therefore, in the above code, demo.__proto__ === demo.prototype.

The Prototype property does this: Prototype contains properties and methods that are shared by all instances it creates.

“Constructor”

The constructor property is also object specific, pointing from an object to a function. The meaning is the constructor that points to the object. The final constructor of all functions points to Function.

When a function is created, its Prototype object is automatically created, which also gets the constructor attribute and points to itself.

So why do we set constructor: ScratchCard manually here?

The reason is that we use this syntax:

ScratchCard.prototype = {}
Copy the code

Causes the automatically set constructor property value to be overwritten. In this case, constructor would point to Object if we didn’t explicitly set it to ScratchCard.

4.2 Canvas coating

Add the following code first:

    functionScratchCard(config) { this.config = { ... } object. assign(this.config, config); + this.canvas = this.config.canvas; + this.ctx = null; + this.offsetX = null; + this.offsetY = null; this._init(); } scratchcard. prototype = {constructor: ScratchCard, // initialize _scratchcard:function() {
+           var that = this;
+           this.ctx = this.canvas.getContext('2d');
+           this.offsetX = this.canvas.offsetLeft;
+           this.offsetY = this.canvas.offsetTop;
+           if(this.config.coverimg) {var coverImg = new Image(); + coverImg.src = this.config.coverImg; + // Read images + coverimg.onload =functionDrawImage (coverImg, 0, 0); drawImage(coverImg, 0, 0); + that.ctx.globalCompositeOperation ='destination-out'; + +}}else{+ // If no image coating is set, use solid color coating + this.ctx.fillstyle = this.config.coverColor; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + this.ctx.globalCompositeOperation ='destination-out'; +}}}Copy the code

The initialization code implements the coating. The key logic here is: If an image coating is set, ignore the solid color coating.

Two canvas apis are involved:

DrawImage is used to draw an image.

FillRect is used to draw a rectangle. Before drawing, the brush is set, that is, the color is set through the fillStyle property.

What does this code mean?

this.ctx.globalCompositeOperation = 'destination-out';
Copy the code

GlobalCompositeOperation is the third point.

Canvas’s globalCompositeOperation

A detailed description of this property can be found at w3School:

value describe
source-over The default. Displays the source image on the target image.
source-atop Displays the source image at the top of the target image. Parts of the source image that lie outside the target image are not visible.
source-in Displays the source image in the target image. Only part of the source image within the target image is displayed, and the target image is transparent.
source-out Displays the source image outside the target image. Only parts of the source image other than the target image are displayed, and the target image is transparent.
destination-over Displays the target image above the source image.
destination-atop Displays the target image at the top of the source image. Portions of the target image other than the source image are not displayed.
destination-in Displays the target image in the source image. Only parts of the target image within the source image are displayed, and the source image is transparent.
destination-out Displays the target image outside the source image. Only parts of the target image outside the source image are displayed, and the source image is transparent.
lighter Display source image + target image.
copy Displays the source image. Ignore the target image.
xor Combine source and target images using xor operations.

It may seem like a bit of a muddle, but it’s similar to specifying how two layers in Photoshop will blend, like who will mask whom, cross section elimination, cross section color fusion, etc.

Please refer to the diagram of W3School. Blue is the target image and red is the source image.

Back to the scratch card, the image coating is the target image, the source image is not set yet, so the source image is fully transparent (the opaque part of the source image is used to remove the target image and make it transparent), so the target image (the image coating) is fully displayed.

The effect is now as shown in the image below. The coating has been applied.

4.3 Adding a Smudge Event

Daub events, in fact, with the TouchStart, TouchMove, TouchEnd events, in order to be compatible with the mouse, mousedown, Mousemove, mouseup.

Modify code:

    functionScratchCard(config) { this.config = { ... } object. assign(this.config, config); this.canvas = this.config.canvas; this.ctx = null; this.offsetX = null; this.offsetY = null; + // Whether the canvas is pressed + this.isdown =false; + // Whether the scratch card is completed + this.done =false; this._init(); } scratchcard. prototype = {constructor: ScratchCard, // initialize _scratchcard:function() {... This.offsety = this.canvas. OffsetTop; + this._addEvent();if(this.config.coverImg) { ... }}, + // Add event + _addEvent:function() {
+           this.canvas.addEventListener('touchstart', this._eventDown.bind(this), { passive: false });
+           this.canvas.addEventListener('touchend', this._eventUp.bind(this), { passive: false });
+           this.canvas.addEventListener('touchmove', this._scratch.bind(this), { passive: false });
+           this.canvas.addEventListener('mousedown', this._eventDown.bind(this), { passive: false });
+           this.canvas.addEventListener('mouseup', this._eventUp.bind(this), { passive: false });
+           this.canvas.addEventListener('mousemove', this._scratch.bind(this), { passive: false });
+       },
+       _eventDown: function(e) {
+           e.preventDefault();
+           this.isDown = true;
+       },
+       _eventUp: function(e) {
+           e.preventDefault();
+           this.isDown = false; }, + // scratch coating + _scratch:function(e) {
+       }
    }
Copy the code

The code is easy to understand, adding event listeners. Set isDown to true when pressed and isDown to false when lifted.

{passive: false} addEventListener {passive: false} That’s number four.

The third addEventListener is the passive property

Initially, the argument convention for addEventListener() looks like this:

el.addEventListener(type, listener, useCapture)
Copy the code
  • El: indicates the event object
  • Type: Event type, click, mouseover, etc
  • Listener: Event handler function, that is, the callback after the event is triggered
  • UseCapture: Boolean, whether captured, default false (bubble)

At the end of 2015, the DOM specification was revised to extend the new options:

el.addEventListener(type, listener, {
    capture: false, // useCapture
    once: false// Whether to set listen on passive:false// Whether to disable the default preventDefault()})Copy the code

The default value for all three properties is false.

Why is there a passive property?

To prevent scrolling, many mobile pages listen for touch events like TouchMove, like this:

document.addEventListener("touchmove".function(e){
    e.preventDefault()
})
Copy the code

Since the Cancelable property of the TouchMove event object is true, its default behavior can be prevented by listeners using the preventDefault() method. What is its default behavior? It usually scrolls the current page (and maybe zooms the page), and if its default behavior is blocked, the page must stand still. The browser has no way of knowing whether or not a listener is calling preventDefault(). All it can do is wait for the listener to finish executing the default behavior, and listener execution takes time, some of which are obvious, causing the page to freeze. Even if the listener is an empty function, this can cause some lag, because empty functions also take time to execute.

When passtive is set to true, preventDefault() in the code is ignored, so the page should be smoother. As shown below, passtive is set to true on the page of the phone on the right.

OK, so the question? Passive: false = passive: false = passive: false

The answer here, to see the chrome official description: www.chromestatus.com/feature/509…

The original text reads as follows:

AddEventListenerOptions defaults passive to false. With this change touchstart and touchmove listeners added to the document will default to passive:true (so that calls to preventDefault will be ignored)..

In the option for addEventListener, passive is false by default. However, if the event is touchStart or Touchmove, passive will be true by default (so preventDefault is ignored).

OK, that’s it. We haven’t blocked the default sliding behavior of pages. If left unchecked, the page will scroll as you slide the scratch card.

4.4 Preventing Page Scrolling

After section 4.3, preventing the page from scrolling is easy. Add the following code to the script in index.html:

+   window.addEventListener('touchmove'.function(e) {
+       e.preventDefault();
+   }, {passive: false});

    new ScratchCard({
        ...(略)
    });
Copy the code

4.5 Achieve the erasing effect

Here’s how to complete the _scratch method:

_scratch: function(e) {
    e.preventDefault();
    var that = this;
    if(! this.done && this.isDown) {if(e.changedTouches) { e = e.changedTouches[e.changedTouches.length - 1]; } var x = (e.clientX + document.body.scrollLeft || e.pageX) - this.offsetX || 0, y = (e.clientY + document.body.scrollTop || e.pageY) - this.offsetY || 0; with(this.ctx) { beginPath() arc(x * that.config.pixelRatio, y * that.config.pixelRatio, that.config.radius * that.config.pixelRatio, 0, Math.PI * 2); fill(); }}}Copy the code

The logic goes like this:

  1. Check that the scratch card is not finished (this.done is false) and is in the pressed state (this.isDown is true).
  2. If multiple contacts exist, the last one is used. Get the last touch with E. hangedTouches.
  3. Calculates the relative coordinates of the contacts in the canvas.
  4. Draw a circle at the contact position on the canvas.

To be clear, multiply pixelRatio to accommodate multiple screens. In this example, the canvas size is 2 times the size, and the coordinates are calculated from the dimensions of the page elements, which differ by exactly 1, so multiply by pixelRatio (pixelRatio = 2).

Remember the globalCompositeOperation in section 4.2? When destination-out is set, the opaque part of the source image will scratch the target image, thus achieving the scratch coating effect of the scratch card.

4.6 Test the proportion of the transparent part of the coating

Although the effect of scraping coating is achieved, it is necessary to detect how much is scraped in real time to determine whether the scratch card is completed.

Continue to modify the code:

    _scratch: function(e) { ... (abbreviated)if(! this.done && this.isDown) { ... With (this.ctx) {... (omitted)} +if(this _getFilledPercentage () > this. Config. ShowAllPercent) {+ enclosing _scratchAll () +}}} + / + / scraping all coating_scratchAll() {
+       var that = this;
+       this.done = true;

+       if(this. Config. FadeOut > 0) {+ / / use CSS opacity to clear first, then using canvas + this. Canvas. The style.css. The transition ='all ' + this.config.fadeOut / 1000 + 's linear';
+           this.canvas.style.opacity = '0';
+           setTimeout(function() {
+               that._clear();
+           }, this.config.fadeOut)
+       } else{+ // use canvas to clear + that._clear(); +} + // Execute the callback function + this.config.donecallBack && this.config.donecallBack (); +}, + // Remove all coating +_clear() { + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); }, + // getFilledpercentage = _getFilledPercentage:function() { + var imgData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); Imgdata.data; // Store all pixels of the current cavnas canvas. + // Store transparent pixel information of the current canvas canvas + var transPixels = []; + // Iterate over all pixel information +for(var i = 0; i < pixels.length; I += 4) {+ // Add transparent pixels to transPixels +if(pixels[i + 3] < 128) { + transPixels.push(pixels[i + 3]); +} +} + // Calculate the percentage of transparent pixels +return (transPixels.length / (pixels.length / 4) * 100).toFixed(2)
+   }
}
Copy the code

Three new methods have been added:

_scratchAll: Clean the coating (all scratched off). If the fadeOut time is set, the canvas will fadeOut through CSS animation, and then the coating will be cleared. If fadeOut is 0, the coating is removed directly.

_clear: Clears the coating. It’s easy. Just draw a rectangle all over the canvas.

_getFilledPercentage: Calculates the percentage of the scraped area. Calculate the proportion of fully transparent pixels by traversing each pixel point of canvas.

This brings us to the fifth point.

Canvas ImageData

Canvas getImageData() method can be used to obtain all pixel information and return the array format. Instead of each element representing one pixel of information in an array, there are four elements representing one pixel of information. Such as:

Data [0] = R value of pixel 1, red (0-255)

Data [1] = G value of pixel 1, green (0-255)

Data [2] = B value of pixel 1, blue (0-255)

Data [3] = A value of pixel 1, alpha channel (0-255; 0 transparent, 255 fully visible)

Data [4] = R value of pixel 2, red (0-255)

.

In this case, there is no intermediate value for transparency, so alpha less than 128 can be considered transparent.

4.7 Precautions

Due to browser security restrictions, Image cannot read local images. Therefore, it needs to be deployed on the server to browse the project through HTTP.

And that’s all for this episode. For the full code, go to GitHub: github.com/Yuezi32/scr…

Have you mastered so much hidden knowledge in a seemingly simple scratch card?

Please follow my personal wechat official number to get the latest articles ^_^