手把手教你實現一個canvas智繪畫板

前言

本文主要介紹:css

  1. 項目介紹
  2. 項目效果展現
  3. 一步步實現項目效果
  4. 踩坑

1、項目介紹

名稱: 智繪畫板html

技術棧: HTML5,CSS3,JavaScript,移動端git

功能描述:github

  • 支持PC端和移動端在線繪畫功能
  • 實現任意選擇畫筆顏色、調整畫筆粗細以及橡皮檫擦除等繪畫功能
  • 實如今線畫板的本地保存功能
  • 支持撤銷和返回操做
  • 自定義背景顏色[這個功能還沒有完善好] 注:本項目僅僅是canvas和JavaScript練手小項目,存在一個問題還沒有解決,橡皮擦把背景層都給擦掉了,但願有大神給一些建議給我,謝謝!

2、項目效果展現

項目地址 預覽地址編程

預覽圖

PC端的預覽圖:canvas

移動端的預覽圖:數組

看完上面的預覽圖和體驗過智繪畫板以爲還能夠的,記得點個贊哦,無論你是否十分激動,反正我是挺激動的,畢竟本身實現出現的項目效果,挺自豪的,說了一堆廢話,下面就能夠動起手來敲代碼,實現本身想要的效果!!!瀏覽器

注:下面實現項目效果主要是關於JavaScript方面的,下面僅僅是提供實現思路的代碼並不是所有代碼app

3、一步步實現項目效果

(一)分析頁面

經過用例圖,咱們知道用戶進入咱們這個網站有哪些功能?函數

用戶能夠進行的操做:

  • 畫畫
  • 改變畫筆的粗細
  • 切換畫筆的顏色
  • 使用橡皮檫擦除不想要的部分
  • 清空畫板
  • 將本身畫的東西保存成圖片
  • 進行撤銷和重作操做
  • 切換畫板背景顏色
  • 兼容移動端(支持觸摸)

(二)進行HTML佈局

我書寫html的同時,引入了css文件和js文件

<!DOCTYPE html>
<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>智繪畫板</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>
    <!-- 自定義背景顏色功能還沒有完善好 -->
    <!--<div class="bg-btn"></div> <div class="color-group" id="bgGroup"> <h3>選擇背景顏色:</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="保存"></button>
            <button class="brush active" id="brush" title="畫筆"></button>
            <button class="eraser" id="eraser" title="橡皮擦"></button>
            <button class="clear" id="clear" title="清屏"></button>
            <button class="undo" id="undo" title="撤銷"></button>
            <button class="redo" id="redo" title="再作"></button>
        </div>
    </div>
    <div class="pen-detail" id="penDetail">
        <i class="closeBtn"></i>
        <p>筆大小</p>
        <span class="circle-box"><i id="thickness"></i></span> <input type="range" id="range1" min="1" max="10" value="1">
        <p>筆顏色</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>不透明度</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>
複製代碼

(三)用CSS美化界面

css代碼能夠根據我的習慣進行美化界面,因此這裏就不寫css的代碼了,你們能夠直接看項目代碼或者從開發者工具中審查元素觀看。若是有問題能夠私聊我,我以爲問題不大。

(四)使用JS實現項目的具體功能

1.準備工做

首先,準備個容器,也就是畫板了,前面的html已經書寫好這個容器,這裏純屬是廢話。

<canvas id="canvas"></canvas>
複製代碼

而後初始化js

let canvas = document.getElementById('canvas');
let context = canvas.getContext('2d');
複製代碼

我打算把畫板作成全屏的,因此接下來設置一下canvas的寬高

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

canvas.width = pageWidth;
canvas.height = pageHeight;
複製代碼

因爲部分IE不支持canvas,若是要兼容IE,咱們能夠建立一個canvas,而後使用excanvas初始化,針對IE加上exCanvas.js,這裏咱們明確不考慮IE。

可是我在電腦上對瀏覽器的窗口進行改變,畫板不會自適應的放縮。解決辦法:

// 記得要執行autoSetSize這個函數哦
function autoSetSize(){
    canvasSetSize();
    // 當執行這個函數的時候,會先設置canvas的寬高
    function canvasSetSize(){
        // 把變化以前的畫布內容copy一份,而後從新畫到畫布上
        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);
    }
    // 在窗口大小改變以後,就會觸發resize事件,從新設置canvas的寬高
    window.onresize = function(){
        canvasSetSize();
    }
}
複製代碼

2.實現畫畫的功能

實現思路:監聽鼠標事件, 用drawLine()方法把記錄的數據畫出來。

  1. 初始化當前畫板的畫筆狀態,painting = false
  2. 當鼠標按下時(mousedown),把painting設爲true,表示正在畫,鼠標沒鬆開。把鼠標點記錄下來。
  3. 當按下鼠標的時候,鼠標移動(mousemove)就把點記錄下來並畫出來。
  4. 若是鼠標移動過快,瀏覽器跟不上繪畫速度,點與點之間會出現間隙,因此咱們須要將畫出的點用線連起來(lineTo())。
  5. 鼠標鬆開的時候(mouseup),把painting設爲false

注:drawCircle這個方法其實能夠不用書寫,這個只是爲了讓你們可以理解開始點擊的位置在哪裏?

function listenToUser() {
    // 定義一個變量初始化畫筆狀態
    let painting = false;
    // 記錄畫筆最後一次的位置
    let lastPoint = {x: undefined, y: undefined};

    // 鼠標按下事件
    canvas.onmousedown = function(e){
        painting = true;
        let x = e.clientX;
        let y = e.clientY;
        lastPoint = {'x':x,'y':y};
        drawCircle(x,y,5);
    }

    // 鼠標移動事件
    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;
        }
    }

    // 鼠標鬆開事件
    canvas.onmouseup = function(){
        painting = false;
    }
}

// 畫點函數
function drawCircle(x,y,radius){
    // 新建一條路徑,生成以後,圖形繪製命令被指向到路徑上生成路徑。
    context.beginPath();
    // 畫一個以(x,y)爲圓心的以radius爲半徑的圓弧(圓),
    // 從startAngle開始到endAngle結束,按照anticlockwise給定的方向(默認爲順時針)來生成。
    context.arc(x,y,radius,0,Math.PI*2);
    // 經過填充路徑的內容區域生成實心的圖形
    context.fill();
    // 閉合路徑以後圖形繪製命令又從新指向到上下文中。
    context.closePath();
}

function drawLine(x1,y1,x2,y2){
    // 設置線條寬度
    context.lineWidth = 10;
    // 設置線條末端樣式。
    context.lineCap = "round";
    // 設定線條與線條間接合處的樣式
    context.lineJoin = "round";
    // moveTo(x,y)將筆觸移動到指定的座標x以及y上
    context.moveTo(x1,y1);
    // lineTo(x, y) 繪製一條從當前位置到指定x以及y位置的直線
    context.lineTo(x2,y2);
    // 經過線條來繪製圖形輪廓
    context.stroke();
    context.closePath();
}
複製代碼

3.實現橡皮擦功能

實現思路:

  1. 獲取橡皮擦元素
  2. 設置橡皮擦初始狀態,eraserEnabled = false
  3. 監聽橡皮擦click事件,點擊橡皮擦,改變橡皮擦狀態,eraserEnabled = true,而且切換class,實現被激活的效果。
  4. eraserEnabledtrue,移動鼠標用context.clearRect()實現了橡皮檫。

可是我發現canvas的API中,能夠清除像素的就是clearRect方法,可是clearRect方法的清除區域矩形,畢竟大部分人的習慣中的橡皮擦都是圓形的,因此就引入了剪輯區域這個強大的功能,也就是clip方法。下面的代碼是使用context.clearRect()實現了 橡皮檫。請看踩坑部分,瞭解如何更好的實現橡皮檫。

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

// 記得要執行listenToUser這個函數哦
function listenToUser() {
   	// ... 表明省略了以前寫的代碼
    // ...

    // 鼠標按下事件
    canvas.onmousedown = function(e){
        // ...
        if(eraserEnabled){//要使用eraser
            context.clearRect(x-5,y-5,10,10)
        }else{
            lastPoint = {'x':x,'y':y}
        }
    }

    // 鼠標移動事件
    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;
        }  
    }

    // ...
}


// 點擊橡皮檫
eraser.onclick = function(){
    eraserEnabled = true;
    eraser.classList.add('active');
    brush.classList.remove('active');
}
複製代碼

4.實現清屏功能

實現思路:

  1. 獲取元素節點。
  2. 點擊清空按鈕清空canvas畫布。
let reSetCanvas = document.getElementById("clear");

// 實現清屏
reSetCanvas.onclick = function(){
    ctx.clearRect(0,0,canvas.width,canvas.height);
    setCanvasBg('white');
}

// 從新設置canvas背景顏色
function setCanvasBg(color) {
    ctx.fillStyle = color;
    ctx.fillRect(0, 0, canvas.width, canvas.height);
}
複製代碼

5.實現保存成圖片功能

實現思路:

  1. 獲取canvas.toDateURL
  2. 在頁面裏建立並插入一個a標籤
  3. a標籤href等於canvas.toDateURL,並添加download屬性
  4. 點擊保存按鈕,a標籤觸發click事件
let save = document.getElementById("save");

// 下載圖片
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();
}
複製代碼

6.實現改變背景顏色的功能

實現思路:

  1. 獲取相應的元素節點。
  2. 給每個class爲bgcolor-item的標籤添加點擊事件,當點擊事件觸發時,改變背景顏色。
  3. 點擊設置背景顏色的div以外的地方,實現隱藏那個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';


// 實現了切換背景顏色
for (let i = 0; i < bgcolorBtn.length; i++) {
    bgcolorBtn[i].onclick = function (e) {
        // 阻止冒泡
        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();
}
複製代碼

7.實現改變畫筆粗細的功能

實現思路:

  1. 實現讓設置畫筆的屬性的對話框出現。
  2. 獲取相應的元素節點。
  3. 當input=range的元素髮生改變的時候,獲取到的值賦值給lWidth。
  4. 而後設置context.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);
}


// 畫線函數
function drawLine(x1,y1,x2,y2){
    // ...
    context.lineWidth = lWidth;
    // ...
}

// 點擊畫筆
brush.onclick = function(){
    eraserEnabled = false;
    brush.classList.add('active');
    eraser.classList.remove('active');
    if(!ifPop){
        // 彈出框
        console.log('彈一彈')
        penDetail.classList.add('active');
    }else{
        penDetail.classList.remove('active');
    }
    ifPop = !ifPop;
}
複製代碼

8.實現改變畫筆顏色的功能

實現思路跟改變畫板背景顏色的思路相似。

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;
            }
        }
    }
}
複製代碼

9.實現改變撤銷和重作的功能

實現思路:

  1. 保存快照:每完成一次繪製操做則保存一份 canvas 快照到 canvasHistory 數組(生成快照使用 canvas 的 toDataURL() 方法,生成的是 base64 的圖片);
  2. 撤銷和反撤銷:把 canvasHistory 數組中對應索引的快照使用 canvas 的 drawImage() 方法重繪一遍;
  3. 繪製新圖像:執行新的繪製操做時,刪除當前位置以後的數組記錄,而後添加新的快照。
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;

// 繪製方法
function canvasDraw(){
    step++;
    if(step < canvasHistory.length){
        canvasHistory.length = step;  // 截斷數組
    }
    // 添加新的繪製到歷史記錄
    canvasHistory.push(canvas.toDataURL());
}

// 撤銷方法
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('不能再繼續撤銷了');
    }
}
// 重作方法
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('已是最新的記錄了');
    }
}
undo.onclick = function(){
    canvasUndo();
}
redo.onclick = function(){
    canvasRedo();
}
複製代碼

10.兼容移動端

實現思路:

  1. 判斷設備是否支持觸摸
  2. true,則使用touch事件;false,則使用mouse事件
// ...
if (document.body.ontouchstart !== undefined) {
    // 使用touch事件
    anvas.ontouchstart = function (e) {
        // 開始觸摸
    }
    canvas.ontouchmove = function (e) {
        // 開始滑動
    }
    canvas.ontouchend = function () {
        // 滑動結束
    }
}else{
    // 使用mouse事件
    // ...
}
// ...
複製代碼

4、踩坑

問題1:在電腦上對瀏覽器的窗口進行改變,畫板不會自適應

解決辦法:

onresize響應事件處理中,獲取到的頁面尺寸參數是變動後的參數 。

當窗口大小發生改變以後,從新設置canvas的寬高,簡單來講,就是窗口改變以後,給canvas.width和canvas.height從新賦值。

// 記得要執行autoSetSize這個函數哦
function autoSetSize(){
    canvasSetSize();
    // 當執行這個函數的時候,會先設置canvas的寬高
    function canvasSetSize(){
        let pageWidth = document.documentElement.clientWidth;
        let pageHeight = document.documentElement.clientHeight;
        
        canvas.width = pageWidth;
        canvas.height = pageHeight;
    }
    // 在窗口大小改變以後,就會觸發resize事件,從新設置canvas的寬高
    window.onresize = function(){
        canvasSetSize();
    }
}
複製代碼

問題2:當繪製線條寬度比較小的時候還好,一旦比較粗就會出現問題

解決辦法:看一下文檔,得出方法,只須要簡單修改一下繪製線條的代碼就行

// 畫線函數
function drawLine(x1,y1,x2,y2){
    context.beginPath();
    context.lineWidth = lWidth;
    //-----加入-----
    // 設置線條末端樣式。
    context.lineCap = "round";
    // 設定線條與線條間接合處的樣式
    context.lineJoin = "round";
    //-----加入-----
    context.moveTo(x1,y1);
    context.lineTo(x2,y2);
    context.stroke();
    context.closePath();
}
複製代碼

問題3:如何實現圓形的橡皮檫?

解決辦法:

canvas的API中,能夠清除像素的就是clearRect方法,可是clearRect方法的清除區域矩形,畢竟大部分人的習慣中的橡皮擦都是圓形的,因此就引入了剪輯區域這個強大的功能,也就是clip方法。用法很簡單: 

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();
複製代碼

上面那段代碼就實現了圓形區域的擦除,也就是先實現一個圓形路徑,而後把這個路徑做爲剪輯區域,再清除像素就好了。有個注意點就是須要先保存繪圖環境,清除完像素後要重置繪圖環境,若是不重置的話之後的繪圖都是會被限制在那個剪輯區域中。

問題4:如何兼容移動端?

1.添加meta標籤

由於瀏覽器初始會將頁面如今手機端顯示時進行縮放,所以咱們能夠在meta標籤中設置meta viewport屬性,告訴瀏覽器不將頁面進行縮放,頁面寬度=用戶設備屏幕寬度

<meta name="viewport" content="width=device-width,
initial-scale=1,user-scalable=no,
maximum-scale=1.0,minimum-scale=1.0"/>

/*
頁面寬度=移動寬度 :width=device-width
用戶不能夠縮放:user-scalable=no
縮放比例:initial-scale=1
最大縮放比例:maximum-scale=1.0
最小縮放比例:minimum-scale=1.0
*/
複製代碼

2.在移動端幾乎使用的都是touch事件,與PC端不一樣

因爲移動端是觸摸事件,因此要用到H5的屬性touchstart/touchmove/touchend,可是PC端只支持鼠標事件,因此要進行特性檢測。

touch事件裏,是經過.touches[0].clientX.touches[0].clientY來獲取座標的,這點要和mouse事件區別開。

問題5:當瀏覽器大小變化時,畫布被清空

解決辦法1:http://js.jirengu.com/dafic/2/edit

解決辦法2:http://js.jirengu.com/worus/2/edit

參考連接:canvas長寬變化時,畫布內容消失

問題6:當橡皮擦移動很快時會變成圓點

參考連接: HTML5 實現橡皮擦的擦除效果

問題7:橡皮擦把背景層都給擦掉了,橡皮擦須要優化

嗯嗯,這個問題還沒有解決,因此我就先把自定義背景顏色的功能取消掉,可是並無用,仍是存在橡皮檫會把背景層給擦掉,但願有大神看到這篇文章,給一點建議和方法,謝謝!

問題8:出現一個問題就是清空以後,從新畫,而後出現原來的畫的東西

這個嘛,問題不大,只不過是我漏寫context.beginPath(); ,也花了一點時間在上面解決bug,讓我想起「代碼千萬行,註釋第一行;編程不規範,同事兩行淚 」,仍是按照文檔操做規範操做好,真香!!!

本文做者 xyyojl

本文若有錯誤之處,請留言,我會及時更正

或者提bug、提需求也是能夠的

以爲對您有幫助的話就點個贊收藏吧!

歡迎轉載或分享,轉載時請註明出處

相關文章
相關標籤/搜索