preface

This article mainly introduces:

  1. Project introduction
  2. Project effect display
  3. Step by step to achieve the project effect
  4. Hit the pit

I. Project introduction

Name: Smart drawing board

Technology stack: HTML5, CSS3, JavaScript, mobile

Function description:

  • Support PC and mobile terminal online drawing function
  • To achieve arbitrary selection of brush color, brush thickness adjustment and eraser erasure and other painting functions
  • Realize the local saving function of online artboard
  • Support undo and return operations
  • Custom background colorNote: This project is just a canvas and JavaScript training project, there is a problem that has not been solved, the eraser has erased the background layer, I hope you can give me some suggestions, thank you!

Ii. Project effect display

Project Address Preview address

preview

PC preview:

Mobile preview:

Look at the preview above and experienced the wisdom drawing board feel ok, remember to point a like oh, whether you are very excited, anyway I am very excited, after all, their own implementation of the project effect, very proud of, said a bunch of nonsense, the following can move hand to knock code, to achieve the effect you want!!

Note: the following implementation project effect is mainly about JavaScript, the following is only to provide the implementation idea of the code, not all the code.

Third, step by step to achieve the effect of the project

(A) Analysis page

Through the use case diagram, we know what functions the user enters our website.

User can perform the following operations:

  • Drawing a picture
  • Change the brush thickness
  • Change the color of the brush
  • Use an eraser to erase the unwanted parts
  • Empty the sketchpad
  • Save your drawings as pictures
  • Do undo and redo operations
  • Toggle the artboard background color
  • Compatible with mobile terminals (touch support)

(2) HTML layout

While I was writing HTML, I introduced CSS files and JS files


      
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="Width = device - width, initial - scale = 1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Think tank drawing board</title>
    <link rel="shortcut icon" href="./image/favicon.png" type="image/x-icon">
    <link rel="stylesheet" href="./css/style.css">
</head>
<body>
    <canvas id="canvas"></canvas>
    <! -- Custom background color is not yet complete -->
    <! --<div class="bg-btn"></div> <div class="color-group" id="bgGroup"> <h3> Select the background color :</h3> <ul class="clearfix"> <li class="bgcolor-item" style="background-color: blue;" ></li> <li class="bgcolor-item" style="background-color: black;" ></li> <li class="bgcolor-item" style="background-color: #FF3333;" ></li> <li class="bgcolor-item" style="background-color: #0066FF;" ></li> <li class="bgcolor-item" style="background-color: #FFFF33;" ></li> <li class="bgcolor-item" style="background-color: #33CC66;" ></li> <li class="bgcolor-item" style="background-color: gray;" ></li> <li class="bgcolor-item" style="background-color: #F34334;" ></li> <li class="bgcolor-item" style="background-color: #fff; Box-shadow: 0 1px 2px 0 rgba(32,33,36,0.28);" ></li> <li class="bgcolor-item" style="background-color: #9B27AC;" ></li> <li class="bgcolor-item" style="background-color: #4CB050;" ></li> <li class="bgcolor-item" style="background-color: #029688;" ></li> </ul> <i class="closeBtn"></i> </div>-->
    <div class="tools">
        <div class="container">
            <button class="save"  id="save" title="Save"></button>
            <button class="brush active" id="brush" title="Brush"></button>
            <button class="eraser" id="eraser" title="Eraser"></button>
            <button class="clear" id="clear" title="Black screen"></button>
            <button class="undo"  id="undo" title="Cancel"></button>
            <button class="redo"  id="redo" title="To do"></button>
        </div>
    </div>
    <div class="pen-detail" id="penDetail">
        <i class="closeBtn"></i>
        <p>Pen size</p>
        <span class="circle-box"><i id="thickness"></i></span> <input type="range" id="range1" min="1" max="10" value="1">
        <p>The pen color</p>
        <ul class="pen-color clearfix">
            <li class="color-item active" style="background-color: black;"></li>
            <li class="color-item" style="background-color: #FF3333;"></li>
            <li class="color-item" style="background-color: #99CC00;"></li>
            <li class="color-item" style="background-color: #0066FF;"></li>
            <li class="color-item" style="background-color: #FFFF33;"></li>
            <li class="color-item" style="background-color: #33CC66;"></li>
        </ul>
        <p>The opacity</p>
        <i class="showOpacity"></i> <input type="range" id="range2" min="1" max="10" value="1">
    </div>
    <script src="./js/main.js"></script>
</body>
</html>
Copy the code

(3) use CSS to beautify the interface

CSS code can be used to beautify the interface according to personal habits, so there is no CSS code here, you can directly look at the project code or review the elements from the developer tools. If you have any questions, you can talk to me privately. I think it’s not a big problem.

(4) Use JS to realize the specific functions of the project

1. Preparation

First of all, prepare a container, which is the sketchboard. The HTML is already written in this container, but this is nonsense.

<canvas id="canvas"></canvas>
Copy the code

Then initialize JS

let canvas = document.getElementById('canvas');
let context = canvas.getContext('2d');
Copy the code

I’m going to make the canvas full screen, so I’m going to set the width and height of the canvas

let pageWidth = document.documentElement.clientWidth;
let pageHeight = document.documentElement.clientHeight;

canvas.width = pageWidth;
canvas.height = pageHeight;
Copy the code

Since some IE does not support canvas, if we want to be compatible with IE, we can create a canvas and initialize it with exCanvas. For IE, we add excanvas. Js.

But when I changed the browser window on my computer, the artboard didn’t fit. The solution:

// Remember to execute the autoSetSize function
function autoSetSize(){
    canvasSetSize();
    // When executing this function, the canvas width and height are set first
    function canvasSetSize(){
        // Copy the content of the canvas before the change, and then paint on the canvas again
        let imgData = context.getImageData(0.0,canvas.width,canvas.height);
        let pageWidth = document.documentElement.clientWidth;
        let pageHeight = document.documentElement.clientHeight;
        
        canvas.width = pageWidth;
        canvas.height = pageHeight;
        context.putImageData(imgData,0.0);
    }
    // After the window size is changed, the resize event is triggered, resetting the width and height of the canvas
    window.onresize = function(){ canvasSetSize(); }}Copy the code

2. Realize the function of drawing

Monitor the mouse event, using the drawLine() method to draw the recorded data.

  1. Initializes the brush state of the current artboard,painting = false.
  2. When the mouse is down (mousedown),paintingSet totrue“Indicates that the mouse is not released while drawing. Record the mouse points.
  3. When the mouse is held down, the mouse moves (mousemove)Record the pointCome down and draw it.
  4. If the mouse moves too fast, the browser can’t keep up with the drawing speed, and there will be gaps between the dots, so we need to connect the dots with lines (lineTo()).
  5. When the mouse is released (mouseup),paintingSet tofalse.

Note: We don’t have to write the drawCircle method, but this is just to give you an idea of where to start clicking.

function listenToUser() {
    // Define a variable to initialize the brush state
    let painting = false;
    // Record the last position of the brush
    let lastPoint = {x: undefined.y: undefined};

    // Mouse down event
    canvas.onmousedown = function(e){
        painting = true;
        let x = e.clientX;
        let y = e.clientY;
        lastPoint = {'x':x,'y':y};
        drawCircle(x,y,5);
    }

    // Mouse movement event
    canvas.onmousemove = function(e){
        if(painting){
            let x = e.clientX;
            let y = e.clientY;
            let newPoint = {'x':x,'y':y}; drawLine(lastPoint.x, lastPoint.y, newPoint.x, newPoint.y); lastPoint = newPoint; }}// Mouse release event
    canvas.onmouseup = function(){
        painting = false; }}// Let me draw a function
function drawCircle(x,y,radius){
    // Create a new path. After generation, the graph drawing command is pointed to the path to generate the path.
    context.beginPath();
    // Draw a circle with (x,y) as center and radius as radius.
    // Start with startAngle and end with endAngle in the direction given by anticLockWise (clockwise by default).
    context.arc(x,y,radius,0.Math.PI*2);
    // Generate a solid graph by filling the content area of the path
    context.fill();
    // After closing the path, the graph drawing command is redirected to the context.
    context.closePath();
}

function drawLine(x1,y1,x2,y2){
    // Set the line width
    context.lineWidth = 10;
    // Set the line end style.
    context.lineCap = "round";
    // Set the style of the indirect line and line
    context.lineJoin = "round";
    // moveTo(x,y) moves the stroke to the specified coordinates x and y
    context.moveTo(x1,y1);
    // lineTo(x, y) Draws a line from the current position to the specified x and y positions
    context.lineTo(x2,y2);
    // Use lines to draw the outline of a graph
    context.stroke();
    context.closePath();
}
Copy the code

3. Implement the eraser function

Implementation ideas:

  1. Gets the eraser element
  2. Set the initial state of the eraser,eraserEnabled = false.
  3. Listening eraserclickEvent, click on the eraser, change the eraser state,eraserEnabled = true, and switch class, implementationTo be activatedThe effect.
  4. eraserEnabledfortrue, move the mouse to usecontext.clearRect()Implements eraser sassafras.

However, I found that in canvas API, the clearRect method can clear pixels, but the clearRect method clears the rectangle of the area, after all, most people are used to the eraser is round, so I introduced a powerful function of clip area, namely the clip method. The following code implements the eraser using context.clearrect (). Please see the step pit section to learn how to better implement eraser.

let eraser = document.getElementById("eraser");
let eraserEnabled = false;

// Remember to execute the listenToUser function
function listenToUser() {
   	/ /... It means that the code you wrote before has been omitted
    // ...

    // Mouse down event
    canvas.onmousedown = function(e){
        // ...
        if(eraserEnabled){// Use eraser
            context.clearRect(x- 5,y- 5.10.10)}else{
            lastPoint = {'x':x,'y':y}
        }
    }

    // Mouse movement event
    canvas.onmousemove = function(e){
        let x = e.clientX;
        let y = e.clientY;
        if(! painting){return}
        if(eraserEnabled){
            context.clearRect(x- 5,y- 5.10.10);
        }else{
            var newPoint = {'x':x,'y':y}; drawLine(lastPoint.x, lastPoint.y,newPoint.x, newPoint.y); lastPoint = newPoint; }}// ...
}


// Click the eraser
eraser.onclick = function(){
    eraserEnabled = true;
    eraser.classList.add('active');
    brush.classList.remove('active');
}
Copy the code

4. Realize screen clearing function

Implementation ideas:

  1. Gets the element node.
  2. Click the Empty button to empty canvas.
let reSetCanvas = document.getElementById("clear");

// Clear the screen
reSetCanvas.onclick = function(){
    ctx.clearRect(0.0,canvas.width,canvas.height);
    setCanvasBg('white');
}

// Reset the canvas background color
function setCanvasBg(color) {
    ctx.fillStyle = color;
    ctx.fillRect(0.0, canvas.width, canvas.height);
}
Copy the code

5. Realize the function of saving into pictures

Implementation ideas:

  1. To obtaincanvas.toDateURL
  2. Create and insert an A tag on the page
  3. A tag href is equal tocanvas.toDateURLAnd add the Download property
  4. Click the Save button and the A label is triggeredclickThe event
let save = document.getElementById("save");

// Download the image
save.onclick = function(){
    let imgUrl = canvas.toDataURL('image/png');
    let saveA = document.createElement('a');
    document.body.appendChild(saveA);
    saveA.href = imgUrl;
    saveA.download = 'mypic'+ (new Date).getTime();
    saveA.target = '_blank';
    saveA.click();
}
Copy the code

6. Realize the function of changing the background color

Implementation ideas:

  1. Gets the corresponding element node.
  2. Add a click event to each bgcolor-item tag of class. When the click event is triggered, change the background color.
  3. Click outside the div where the background color is set to hide that div.
let selectBg = document.querySelector('.bg-btn');
let bgGroup = document.querySelector('.color-group');
let bgcolorBtn = document.querySelectorAll('.bgcolor-item');
let penDetail = document.getElementById("penDetail");
let activeBgColor = '#fff';


// The background color is changed
for (let i = 0; i < bgcolorBtn.length; i++) {
    bgcolorBtn[i].onclick = function (e) {
        // Stop the bubble
        e.stopPropagation();
        for (let i = 0; i < bgcolorBtn.length; i++) {
            bgcolorBtn[i].classList.remove("active");
            this.classList.add("active");
            activeBgColor = this.style.backgroundColor; setCanvasBg(activeBgColor); }}}document.onclick = function(){
    bgGroup.classList.remove('active');
}

selectBg.onclick = function(e){
    bgGroup.classList.add('active');
    e.stopPropagation();
}
Copy the code

7. Realize the function of changing the brush thickness

Implementation ideas:

  1. Implement a dialog box to set the brush properties.
  2. Gets the corresponding element node.
  3. When the input=range element changes, the obtained value is assigned to lWidth.
  4. And then setcontext.lineWidth = lWidth.
let range1 = document.getElementById('range1');
let lWidth = 2;
let ifPop = false;

range1.onchange = function(){
    console.log(range1.value);
    console.log(typeof range1.value)
    thickness.style.transform = 'scale('+ (parseInt(range1.value)) +') ';
    console.log(thickness.style.transform )
    lWidth = parseInt(range1.value*2);
}


// Draw the line function
function drawLine(x1,y1,x2,y2){
    // ...
    context.lineWidth = lWidth;
    // ...
}

// Click the brush
brush.onclick = function(){
    eraserEnabled = false;
    brush.classList.add('active');
    eraser.classList.remove('active');
    if(! ifPop){/ / the pop-up box
        console.log('Bounce')
        penDetail.classList.add('active');
    }else{
        penDetail.classList.remove('active'); } ifPop = ! ifPop; }Copy the code

8. Realize the function of changing the brush color

The idea is similar to changing the color of the artboard background.

let aColorBtn = document.getElementsByClassName("color-item");

getColor();

function getColor(){
    for (let i = 0; i < aColorBtn.length; i++) {
        aColorBtn[i].onclick = function () {
            for (let i = 0; i < aColorBtn.length; i++) {
                aColorBtn[i].classList.remove("active");
                this.classList.add("active");
                activeColor = this.style.backgroundColor; ctx.fillStyle = activeColor; ctx.strokeStyle = activeColor; }}}}Copy the code

9. Implement change undo and redo functions

Implementation ideas:

  1. Save snapshot: Save a canvas snapshot tocanvasHistoryArray (generate snapshot using CanvastoDataURL()Method, which generates a base64 image);
  2. Undo and anti-undo: putcanvasHistoryThe snapshot of the corresponding index in the array is canvasdrawImage()Method redraw;
  3. Draw new image: When a new draw operation is performed, the array record after the current position is deleted and a new snapshot is added.
let undo = document.getElementById("undo");
let redo = document.getElementById("redo");

// ...
canvas.ontouchend = function () {
        painting = false;
        canvasDraw();
}

// ...
canvas.onmouseup = function(){
        painting = false;
        canvasDraw();
}

let canvasHistory = [];
let step = - 1;

// Draw method
function canvasDraw(){
    step++;
    if(step < canvasHistory.length){
        canvasHistory.length = step;  // Truncate the array
    }
    // Add a new draw to the history
    canvasHistory.push(canvas.toDataURL());
}

// Undo method
function canvasUndo(){
    if(step > 0){
        step--;
        / / CTX. ClearRect (0, 0, canvas width, canvas, height);
        let canvasPic = new Image();
        canvasPic.src = canvasHistory[step];
        canvasPic.onload = function () { ctx.drawImage(canvasPic, 0.0); }
        undo.classList.add('active');
    }else{
        undo.classList.remove('active');
        alert('No more undo.'); }}// Redo the method
function canvasRedo(){
    if(step < canvasHistory.length - 1){
        step++;
        let canvasPic = new Image();
        canvasPic.src = canvasHistory[step];
        canvasPic.onload = function () { 
            / / CTX. ClearRect (0, 0, canvas width, canvas, height);
            ctx.drawImage(canvasPic, 0.0);
        }
        redo.classList.add('active');
    }else {
        redo.classList.remove('active')
        alert('It's already up to date.');
    }
}
undo.onclick = function(){
    canvasUndo();
}
redo.onclick = function(){
    canvasRedo();
}
Copy the code

10. Compatible with mobile terminals

Implementation ideas:

  1. Determine whether the device supports touch
  2. true, use thetouchEvents;false, use themouseThe event
// ...
if (document.body.ontouchstart ! = =undefined) {
    // Use the touch event
    anvas.ontouchstart = function (e) {
        // Start touching
    }
    canvas.ontouchmove = function (e) {
        // Start the slide
    }
    canvas.ontouchend = function () {
        // The slide ends}}else{
    // Use the mouse event
    // ...
}
// ...
Copy the code

Fourth, hit the pit

Problem 1: The artboard does not adapt to changes made to the browser window on the computer

The solution:

Onresize In response event processing, the page size parameter obtained is the changed parameter.

When the window size changes, reset the width and height of canvas. Simply put, after the window changes, reset the values of canvas.width and Canvas. height.

// Remember to execute the autoSetSize function
function autoSetSize(){
    canvasSetSize();
    // When executing this function, the canvas width and height are set first
    function canvasSetSize(){
        let pageWidth = document.documentElement.clientWidth;
        let pageHeight = document.documentElement.clientHeight;
        
        canvas.width = pageWidth;
        canvas.height = pageHeight;
    }
    // After the window size is changed, the resize event is triggered, resetting the width and height of the canvas
    window.onresize = function(){ canvasSetSize(); }}Copy the code

Problem 2: It’s ok when the line width is small, but it will be a problem when the line width is thick

Solution: Take a look at the documentation and figure out the method, which requires a simple change to the code that draws the lines

 // Draw the line function
function drawLine(x1,y1,x2,y2){
    context.beginPath();
    context.lineWidth = lWidth;
    / / -- -- -- -- -- add -- -- -- -- --
    // Set the line end style.
    context.lineCap = "round";
    // Set the style of the indirect line and line
    context.lineJoin = "round";
    / / -- -- -- -- -- add -- -- -- -- --
    context.moveTo(x1,y1);
    context.lineTo(x2,y2);
    context.stroke();
    context.closePath();
}
Copy the code

Question 3: How to achieve a round eraser?

The solution:

In canvas API, the clearRect method is used to clear pixels, but the clearRect method is used to clear the rectangle, after all, most people are used to the eraser is round, so the clip area is introduced, which is a powerful function. It’s simple to use:

ctx.save()
ctx.beginPath()
ctx.arc(x2,y2,a,0.2*Math.PI);
ctx.clip()
ctx.clearRect(0.0,canvas.width,canvas.height);
ctx.restore();
Copy the code

The above code implements the circle area erase, that is, first implement a circle path, and then the path as the clipping area, and then clear the pixels. One thing to note is that you need to save the drawing environment first and reset the drawing environment after you have cleared the pixels. If you do not reset the drawing environment, all subsequent drawings will be confined to that clipping area.

Question 4: How is it compatible with mobile terminals?

1. Add meta tags

Since the browser will initially scale the page when it is displayed on the mobile, we can set the Meta ViewPort property in the Meta tag to tell the browser not to scale the page and that the page width is equal to the user’s device screen width

<meta name="viewport" content="width=device-width, initial-scale=1,user-scalable=no, /> /* scalable: Scalable = user-Scalable =no scale: maximum-scale=1.0,minimum-scale=1.0"/> /* scalable: Scalable: Scalable: Scalable: Scalable: Scalable: Scalable: Scalable: Scalable: Scalable: Scalable: Scalable: Scalable: Scalable: Scalable Initial-scale =1 Maximum-scale =1.0 Minimum-scale =1.0 */Copy the code

2. Almost all touch events are used on mobile terminal, which is different from PC terminal

Is due to the mobile terminal touch events, so want to use the attribute of H5 touchstart/touchmove/touchend, but PC only support mouse events, so for feature detection.

Touches [0].clientx and.touches[0].clienty are used to retrieve coordinates in the Touch event, as distinct from the Mouse event.

Problem 5: When the browser size changes, the canvas is cleared

The solution 1:http://js.jirengu.com/dafic/2/edit

The solution 2:http://js.jirengu.com/worus/2/edit

The canvas content disappears when the width and length of the canvas change

Problem 6: The eraser becomes a dot when it moves quickly

Reference link: HTML5 to achieve the eraser effect

Problem 7: The eraser has erased all the background layers. The eraser needs to be optimized

This problem hasn’t been solved yet, so I canceled the function of customizing the background color first, but it didn’t work, there is still eraser will erase the background layer, I hope you can read this article, give some suggestions and methods, thank you!

Problem 8: There is a problem that after clearing, redraw, and then the original painting things

Well, it’s not a problem, but I left out context.beginPath(); I also spent a little time fixing bugs on it, which reminded me of “Ten million lines of code, comment the first line; Programming is not standard, colleagues two lines of tears “, or in accordance with the operation of the document specifications, really sweet!!

xyyojl

If there are any mistakes in this article, please leave a message and I will correct them in time

It is also ok to mention bugs and requirements

If you think it is helpful, you can click a like or collect it!

Welcome to reprint or share, reprint please indicate the source