preface

For a long time did not share the article, because just changed a job to go to the goose factory, every day busy with seven, eight, eight things, in recent days also finally be stable down, I would like to share some of the recent period of time in the work may be useful to everyone’s gadgets.

Recently, I made a signature function in the project. In fact, I also made a signature function a few years ago. At that time, I just graduated from college and didn’t know very well. Before the front end, there was a popular saying “Don’t repeat the wheel”. Just like Jack Ma said he didn’t like money, only when he was rich can he say he didn’t like money. Only when you understand the principle can you say don’t repeat the wheel, in fact, it is necessary to repeat the wheel, in this process, you can learn a lot of ideas and knowledge.

Today, I will share how to realize [signature function], which is still used in many scenarios. Before, I used electronic signature to handle social security, bank card and other services on the bank machine, and my recent employment contract was also signed online. In the future, electronic signature will be more and more common.

The signature function uses Canvas technology. Last Christmas, I also shared an article about Canvas to achieve simple snow effects using Canvas. If you are interested, you can have a look.

To realize the signature

First look at the effect: to achieve a preview, undo, empty, save, cutting functions

1. Signature function

Signature drawing is the core function. On the mobile side, our finger is like a pen, so touchStart, TouchMove and TouchEnd events are used for drawing. If the mouse is like a pen on the PC side, mousedown, Mousemove and Mouseup events are used for drawing. This article takes the mobile terminal as an example. The principle of the PC terminal is the same, but the triggering events are different.

The basic idea is to initialize the drawn DOM element first, obtain the current clientX and clientY coordinates through the touch event, then draw the path through the Canvas drawing path API, and record the current Canvas for subsequent functions such as cancellation after each touch event.

The code:

// html
<div id="app">
    <div class="area">
        <canvas ref="canvas"
        @touchstart="touchStart"
        @touchmove="touchMove"
        @touchend="touchEnd">
    </canvas>
    <! -- <canvas ref="canvas" @mousedown="mouseDown" @mousemove="mouseMove" @mouseup="mouseUp"> </canvas> -->
    </div>
    <button class="btn" @click="preview">preview</button>
    <button class="btn" @click="revert">undo</button>
    <button class="btn" @click="clear">empty</button>
    <button class="btn" @click="save">save</button>
    <button class="btn" @click="clip">tailoring</button>
    <div class="preview">
        <img src="" alt="">
    </div>
</div>
Copy the code
new Vue({
    el: '#app'.props: {
        lineWidth: {
            type: Number.default: 4
        },
        lineColor: {
            type: String.default: '# 000'
        },
        isCrop: {
            type: Boolean.default: false
        }
    },
    data () {
        return {
            canvasRect: null.// Width and height clientRect data
            ctx: null.// Brush object
            startX: 0.startY: 0.endX: 0.endY: 0.storageSteps: [].// Log each step
            isDrawing: false.// Whether to draw
            isEmpty: true.// Whether the artboard is empty
        }
    },
    mounted () {
        this.init()

        // Release the mouse outside the palette and freeze the brush
        document.onmouseup = () = > {
            this.isDrawing = false}},methods: {
        init () {
            const canvas = this.$refs.canvas;

            this.canvasRect = canvas.getBoundingClientRect();
            console.log(this.canvasRect)

            canvas.width = this.canvasRect.width;
            canvas.height = this.canvasRect.height;

            this.ctx = canvas.getContext('2d')},// mobile
        touchStart (e) {
            e.preventDefault();
            this.startX = e.targetTouches[0].clientX - this.canvasRect.left;
            this.startY = e.targetTouches[0].clientY - this.canvasRect.top;

            this.endX = this.startX;
            this.endY = this.startY;

            this.draw();
        },
        touchMove (e) {
            e.preventDefault();

            this.endX = e.targetTouches[0].clientX - this.canvasRect.left;
            this.endY = e.targetTouches[0].clientY - this.canvasRect.top;
            this.draw()
            this.startX = this.endX;
            this.startY = this.endY;
        },
        touchEnd (e) {
            e.preventDefault();
            // console.log(e)
            this.endX = e.changedTouches[0].clientX - this.canvasRect.left;
            this.endY = e.changedTouches[0].clientY - this.canvasRect.top;

            let imgData = this.ctx.getImageData(0.0.this.canvasRect.width, this.canvasRect.height)
            console.log(imgData)
            this.storageSteps.push(imgData)
            // console.log(this.storageSteps)
        },
        / / to draw
        draw () {
            this.ctx.beginPath();
            this.ctx.moveTo(this.startX, this.startY);
            this.ctx.lineTo(this.endX, this.endY);
            this.ctx.lineCap = 'round';
            this.ctx.lineJoin = 'round';
            this.ctx.lineWidth = this.lineWidth;
            this.ctx.strokeStyle = this.lineColor;
            this.ctx.stroke();
            this.ctx.closePath();

            this.isEmpty = false; }}})Copy the code

Two, empty function

To clear the Canvas, you can use the clearRect(x, y, width, height) method in the Canvas API.

/ / to empty
clear () {
	this.ctx.clearRect(0.0.this.canvasRect.width, this.canvasRect.height);

	this.storageSteps = [];  // Clear the step record
	this.isEmpty = true;  // Clear the tag
}
Copy the code

Undo function

How it works: We record information about the current canvas after the touched touched event, store it in the storageSteps array, and redraw the previous canvas information when we click undo, using the getImageData() and putImageData() methods.

/ / touched
touchEnd (e) {
    e.preventDefault();
    // console.log(e)
    this.endX = e.changedTouches[0].clientX - this.canvasRect.left;
    this.endY = e.changedTouches[0].clientY - this.canvasRect.top;

    let imgData = this.ctx.getImageData(0.0.this.canvasRect.width, this.canvasRect.height) Record the current canvas information
    console.log(imgData)
    this.storageSteps.push(imgData)
    // console.log(this.storageSteps)
}
....省略....
/ / cancel
revert () {
    this.storageSteps.pop()
    const len = this.storageSteps.length;
    if (len) {
            this.ctx.putImageData(this.storageSteps[len - 1].0.0);
    } else {
            this.clear()
    }
    // console.log('>>>', this.storageSteps)
}
Copy the code

4. Preview function

How it works: Converts canvas information to Base64 using the toDataURL() method, which returns a data URI containing the image presentation.

/ / the preview
preview () {
    const base64 = this.$refs.canvas.toDataURL('image/png');
    console.log(base64)
    const img = document.querySelector('.preview img');
    img.src = base64;
    img.width = this.canvasRect.width;
    img.height = this.canvasRect.height;
}
Copy the code

Five, save function

The implementation principle is the same as the preview function, that is, the Canvas Canvas data into pictures, and then use the A tag to download down

/ / save
save () {
    if (this.isEmpty) {
        console.log('Canvas is empty! ')
        return
    }
    const name = prompt('Please enter a name'.'signature canvas');
    if (name && name.trim()) {
        // console.log(name)
        const a = document.createElement('a');
        a.download = name;
        a.href = this.$refs.canvas.toDataURL('image/png');
        // console.log(a)
        a.dispatchEvent(new MouseEvent('click')); // There may be compatibility with IE to render tags and trigger click events}}Copy the code

Six, cutting function

What is the tailoring function? The previous function is no matter preview or save. When converting canvas canvas into picture, the picture size is the size of canvas canvas. When saving or downloading signature, there are many transparent parts at the edge of picture.

The ImageData() constructor creates an ImageData object with the given Uint8ClampedArray and contains the size of the image. The Uint8ClampedArray length = 4 x width x height. If no array is given, a black rectangle image is created that is “fully transparent” (because the opacity value is 0). The imageData. data attribute describes a one-dimensional array containing RGBA order data represented as integers from 0 to 255 inclusive.

/ / cutting
clip () {
    if (this.isEmpty) {
            console.log('Canvas is empty! ')
            return
    }
    const imgData = this.ctx.getImageData(0.0.this.canvasRect.width, this.canvasRect.height);
    const clipArea = this.getCropArea(imgData.data)
    console.log(clipArea)

    const _canvas = document.createElement('canvas')
    _canvas.width = clipArea.x2 - clipArea.x1;
    _canvas.height = clipArea.y2 - clipArea.y1;
    const _ctx = _canvas.getContext('2d');

    const imageData = this.ctx.getImageData(clipArea.x1, clipArea.y1, _canvas.width, _canvas.height);
    _ctx.putImageData(imageData, 0.0)
    const base64 = _canvas.toDataURL('image/png');

    // const name = prompt(' Please enter a name ', 'canvas signature ');
    // if (name && name.trim()) {
    // const a = document.createElement('a');
    // a.download = name;
    // a.href = base64;
    // a.dispatchEvent(new MouseEvent('click')); // There may be compatibility with IE to render tags and trigger click events
    // }

    const img = document.querySelector('.preview img');
    img.src = base64;
    img.width = _canvas.width;
    img.height = _canvas.height;
},
// Calculate blank space
getCropArea (imgData) {
    let x1 = Math.round(this.canvasRect.width);
    let y1 = Math.round(this.canvasRect.height);
    let x2 = 0;
    let y2 = 0;
    console.log([x1, y1, x2, y2])

    for (let i = 0; i < Math.round(this.canvasRect.width); i++) {
        for (let j = 0; j < Math.round(this.canvasRect.height); j++) {
          let pos = (i + Math.round(this.canvasRect.width) * j) * 4;
          if (imgData[pos] > 0 || imgData[pos + 1] > 0 || imgData[pos + 2] || imgData[pos + 3] > 0) { // Determine that the pixels in row j and column I are not transparent
            // Find the upper left and lower right coordinates with color
            x1 = Math.min(i, x1);
            y1 = Math.min(j, y1);
            x2 = Math.max(i, x2)
            y2 = Math.max(j, y2)
          }
        }
      }
      x1++
      y1++
      x2++
      y2++
      return { x1, y1, x2, y2 } // Since the loop starts at 0, we think of rows and columns as starting at 1}}Copy the code

At the end

The above code is a small demo written by VUE. There may be some compatibility problems and it is not packaged into components, but it has some reference significance. It is used for production and can be packaged into components by itselfGitHub


This article is the author summarizes the compilation, if there is bias, welcome to leave a message correction, if you think this article is useful to you, might as well point a thumbs-up ~

About the author: GitHub Jane Book Nuggets