本文主要介紹:
一、項(xiàng)目介紹
名稱:智繪畫板
技術(shù)棧:HTML5,CSS3,JavaScript,移動(dòng)端
功能描述:
二、項(xiàng)目效果展示
項(xiàng)目地址 預(yù)覽地址
預(yù)覽圖
PC端的預(yù)覽圖:
移動(dòng)端的預(yù)覽圖:
看完上面的預(yù)覽圖和體驗(yàn)過(guò) 智繪畫板 覺(jué)得還可以的,記得點(diǎn)個(gè)贊哦,不管你是否十分激動(dòng),反正我是挺激動(dòng)的,畢竟自己實(shí)現(xiàn)出現(xiàn)的項(xiàng)目效果,挺自豪的,說(shuō)了一堆廢話,下面就可以動(dòng)起手來(lái)敲代碼,實(shí)現(xiàn)自己想要的效果!!!
注:下面實(shí)現(xiàn)項(xiàng)目效果主要是關(guān)于JavaScript方面的,下面僅僅是提供 實(shí)現(xiàn)思路的代碼 , 并非全部代碼 。
三、一步步實(shí)現(xiàn)項(xiàng)目效果
(一)分析頁(yè)面
通過(guò) 用例圖 ,我們知道用戶進(jìn)入我們這個(gè)網(wǎng)站有哪些功能?
用戶可以進(jìn)行的操作:
(二)進(jìn)行HTML布局
我書寫html的同時(shí),引入了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" <button class="brush active" id="brush" <button class="eraser" id="eraser" <button class="clear" id="clear" <button class="undo" id="undo" <button class="redo" id="redo" </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代碼可以根據(jù)個(gè)人習(xí)慣進(jìn)行美化界面,所以這里就不寫css的代碼了,大家可以直接看 項(xiàng)目代碼 或者從開(kāi)發(fā)者工具中審查元素觀看。如果有問(wèn)題可以私聊我,我覺(jué)得問(wèn)題不大。
(四)使用JS實(shí)現(xiàn)項(xiàng)目的具體功能
1.準(zhǔn)備工作
首先,準(zhǔn)備個(gè)容器,也就是畫板了,前面的html已經(jīng)書寫好這個(gè)容器,這里純屬是廢話。
<canvas id="canvas"></canvas>
然后初始化js
let canvas = document.getElementById('canvas');let context = canvas.getContext('2d');
我打算把畫板做成全屏的,所以接下來(lái)設(shè)置一下 canvas
的寬高
let pageWidth = document.documentElement.clientWidth;let pageHeight = document.documentElement.clientHeight;canvas.width = pageWidth;canvas.height = pageHeight;
由于部分IE不支持 canvas
,如果要兼容IE,我們可以創(chuàng)建一個(gè) canvas
,然后使用 excanvas
初始化,針對(duì)IE加上exCanvas.js,這里我們明確不考慮IE。
但是我在電腦上對(duì)瀏覽器的窗口進(jìn)行改變,畫板不會(huì)自適應(yīng)的放縮。解決辦法:
// 記得要執(zhí)行autoSetSize這個(gè)函數(shù)哦function autoSetSize(){ canvasSetSize(); // 當(dāng)執(zhí)行這個(gè)函數(shù)的時(shí)候,會(huì)先設(shè)置canvas的寬高 function canvasSetSize(){ let pageWidth = document.documentElement.clientWidth; let pageHeight = document.documentElement.clientHeight; canvas.width = pageWidth; canvas.height = pageHeight; } // 在窗口大小改變之后,就會(huì)觸發(fā)resize事件,重新設(shè)置canvas的寬高 window.onresize = function(){ canvasSetSize(); }}
2.實(shí)現(xiàn)畫畫的功能
實(shí)現(xiàn)思路:監(jiān)聽(tīng)鼠標(biāo)事件, 用 drawLine()
方法把記錄的數(shù)據(jù)畫出來(lái)。
painting = false
。mousedown
),把 painting
設(shè)為 true
,表示正在畫,鼠標(biāo)沒(méi)松開(kāi)。把鼠標(biāo)點(diǎn)記錄下來(lái)。mousemove
)就 把點(diǎn)記錄 下來(lái)并畫出來(lái)。 如果鼠標(biāo)移動(dòng)過(guò)快,瀏覽器跟不上繪畫速度,點(diǎn)與點(diǎn)之間會(huì)出現(xiàn)間隙,所以我們需要將畫出的點(diǎn)用線連起來(lái)( lineTo()
)。mouseup
),把 painting
設(shè)為 false
。 注: drawCircle
這個(gè)方法其實(shí)可以不用書寫,這個(gè)只是為了讓大家能夠理解開(kāi)始點(diǎn)擊的位置在哪里?
function listenToUser() { // 定義一個(gè)變量初始化畫筆狀態(tài) let painting = false; // 記錄畫筆最后一次的位置 let lastPoint = {x: undefined, y: undefined}; // 鼠標(biāo)按下事件 canvas.onmousedown = function(e){ painting = true; let x = e.clientX; let y = e.clientY; lastPoint = {'x':x,'y':y}; drawCircle(x,y,5); } // 鼠標(biāo)移動(dòng)事件 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; } } // 鼠標(biāo)松開(kāi)事件 canvas.onmouseup = function(){ painting = false; }}// 畫點(diǎn)函數(shù)function drawCircle(x,y,radius){ // 新建一條路徑,生成之后,圖形繪制命令被指向到路徑上生成路徑。 context.beginPath(); // 畫一個(gè)以(x,y)為圓心的以radius為半徑的圓弧(圓), // 從startAngle開(kāi)始到endAngle結(jié)束,按照anticlockwise給定的方向(默認(rèn)為順時(shí)針)來(lái)生成。 context.arc(x,y,radius,0,Math.PI*2); // 通過(guò)填充路徑的內(nèi)容區(qū)域生成實(shí)心的圖形 context.fill(); // 閉合路徑之后圖形繪制命令又重新指向到上下文中。 context.closePath();}function drawLine(x1,y1,x2,y2){ // 設(shè)置線條寬度 context.lineWidth = 10; // 設(shè)置線條末端樣式。 context.lineCap = "round"; // 設(shè)定線條與線條間接合處的樣式 context.lineJoin = "round"; // moveTo(x,y)將筆觸移動(dòng)到指定的坐標(biāo)x以及y上 context.moveTo(x1,y1); // lineTo(x, y) 繪制一條從當(dāng)前位置到指定x以及y位置的直線 context.lineTo(x2,y2); // 通過(guò)線條來(lái)繪制圖形輪廓 context.stroke(); context.closePath();}
3.實(shí)現(xiàn)橡皮擦功能
實(shí)現(xiàn)思路:
eraserEnabled = false
。click
事件,點(diǎn)擊橡皮擦,改變橡皮擦狀態(tài), eraserEnabled = true
,并且切換class,實(shí)現(xiàn) 被激活 的效果。eraserEnabled
為 true
時(shí),移動(dòng)鼠標(biāo)用 context.clearRect()
實(shí)現(xiàn)了 橡皮檫。 但是我發(fā)現(xiàn)canvas的API中,可以清除像素的就是clearRect方法,但是clearRect方法的清除區(qū)域矩形,畢竟大部分人的習(xí)慣中的橡皮擦都是圓形的,所以就引入了剪輯區(qū)域這個(gè)強(qiáng)大的功能,也就是clip方法。下面的代碼是使用 context.clearRect()
實(shí)現(xiàn)了 橡皮檫。請(qǐng)看踩坑部分,了解如何更好的實(shí)現(xiàn)橡皮檫。
let eraser = document.getElementById("eraser");let eraserEnabled = false;// 記得要執(zhí)行l(wèi)istenToUser這個(gè)函數(shù)哦function listenToUser() { // ... 代表省略了之前寫的代碼 // ... // 鼠標(biāo)按下事件 canvas.onmousedown = function(e){ // ... if(eraserEnabled){//要使用eraser context.clearRect(x-5,y-5,10,10) }else{ lastPoint = {'x':x,'y':y} } } // 鼠標(biāo)移動(dòng)事件 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; } } // ...}// 點(diǎn)擊橡皮檫eraser.onclick = function(){ eraserEnabled = true; eraser.classList.add('active'); brush.classList.remove('active');}
4.實(shí)現(xiàn)清屏功能
實(shí)現(xiàn)思路:
獲取元素節(jié)點(diǎn)。
點(diǎn)擊清空按鈕清空canvas畫布。
let reSetCanvas = document.getElementById("clear");// 實(shí)現(xiàn)清屏reSetCanvas.onclick = function(){ ctx.clearRect(0,0,canvas.width,canvas.height); setCanvasBg('white');}// 重新設(shè)置canvas背景顏色function setCanvasBg(color) { ctx.fillStyle = color; ctx.fillRect(0, 0, canvas.width, canvas.height);}
5.實(shí)現(xiàn)保存成圖片功能
實(shí)現(xiàn)思路:
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.實(shí)現(xiàn)改變背景顏色的功能
實(shí)現(xiàn)思路:
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';// 實(shí)現(xiàn)了切換背景顏色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.實(shí)現(xiàn)改變畫筆粗細(xì)的功能
實(shí)現(xiàn)思路:
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);}// 畫線函數(shù)function drawLine(x1,y1,x2,y2){ // ... context.lineWidth = lWidth; // ...}// 點(diǎn)擊畫筆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.實(shí)現(xiàn)改變畫筆顏色的功能
實(shí)現(xiàn)思路跟 改變畫板背景顏色 的思路類似。
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.實(shí)現(xiàn)改變撤銷和重做的功能
實(shí)現(xiàn)思路:
canvasHistory
數(shù)組(生成快照使用 canvas 的 toDataURL()
方法,生成的是 base64 的圖片);canvasHistory
數(shù)組中對(duì)應(yīng)索引的快照使用 canvas 的 drawImage()
方法重繪一遍;let undo = document.getElementById("undo");let redo = document.getElementById("redo");// ...canvas.onmouseup = function(){ painting = false; canvasDraw();}let canvasHistory = [];let step = -1;// 繪制方法function canvasDraw(){ step++; if(step < canvasHistory.length){ canvasHistory.length = step; // 截?cái)鄶?shù)組 } // 添加新的繪制到歷史記錄 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('不能再繼續(xù)撤銷了'); }}// 重做方法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('已經(jīng)是最新的記錄了'); }}undo.onclick = function(){ canvasUndo();}redo.onclick = function(){ canvasRedo();}
10.兼容移動(dòng)端
實(shí)現(xiàn)思路:
true
,則使用 touch
事件; false
,則使用 mouse
事件// ...if (document.body.ontouchstart !== undefined) { // 使用touch事件 anvas.ontouchstart = function (e) { // 開(kāi)始觸摸 } canvas.ontouchmove = function (e) { // 開(kāi)始滑動(dòng) } canvas.ontouchend = function () { // 滑動(dòng)結(jié)束 }}else{ // 使用mouse事件 // ...}// ...
四、踩坑
問(wèn)題1:在電腦上對(duì)瀏覽器的窗口進(jìn)行改變,畫板不會(huì)自適應(yīng)
解決辦法:
onresize響應(yīng)事件處理中,獲取到的頁(yè)面尺寸參數(shù)是變更后的參數(shù) 。
當(dāng)窗口大小發(fā)生改變之后,重新設(shè)置canvas的寬高,簡(jiǎn)單來(lái)說(shuō),就是窗口改變之后,給canvas.width和canvas.height重新賦值。
// 記得要執(zhí)行autoSetSize這個(gè)函數(shù)哦function autoSetSize(){ canvasSetSize(); // 當(dāng)執(zhí)行這個(gè)函數(shù)的時(shí)候,會(huì)先設(shè)置canvas的寬高 function canvasSetSize(){ let pageWidth = document.documentElement.clientWidth; let pageHeight = document.documentElement.clientHeight; canvas.width = pageWidth; canvas.height = pageHeight; } // 在窗口大小改變之后,就會(huì)觸發(fā)resize事件,重新設(shè)置canvas的寬高 window.onresize = function(){ canvasSetSize(); }}
問(wèn)題2:當(dāng)繪制線條寬度比較小的時(shí)候還好,一旦比較粗就會(huì)出現(xiàn)問(wèn)題
解決辦法:看一下文檔,得出方法,只需要簡(jiǎn)單修改一下 繪制線條的代碼 就行
// 畫線函數(shù)function drawLine(x1,y1,x2,y2){ context.beginPath(); context.lineWidth = lWidth; //-----加入----- // 設(shè)置線條末端樣式。 context.lineCap = "round"; // 設(shè)定線條與線條間接合處的樣式 context.lineJoin = "round"; //-----加入----- context.moveTo(x1,y1); context.lineTo(x2,y2); context.stroke(); context.closePath();}
問(wèn)題3:如何實(shí)現(xiàn)圓形的橡皮檫?
解決辦法:
canvas的API中,可以清除像素的就是clearRect方法,但是clearRect方法的清除區(qū)域矩形,畢竟大部分人的習(xí)慣中的橡皮擦都是圓形的,所以就引入了剪輯區(qū)域這個(gè)強(qiáng)大的功能,也就是clip方法。用法很簡(jiǎn)單:
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();
上面那段代碼就實(shí)現(xiàn)了圓形區(qū)域的擦除,也就是先實(shí)現(xiàn)一個(gè)圓形路徑,然后把這個(gè)路徑作為剪輯區(qū)域,再清除像素就行了。有個(gè)注意點(diǎn)就是需要先保存繪圖環(huán)境,清除完像素后要重置繪圖環(huán)境,如果不重置的話以后的繪圖都是會(huì)被限制在那個(gè)剪輯區(qū)域中。
問(wèn)題4:如何兼容移動(dòng)端?
1.添加meta標(biāo)簽
因?yàn)闉g覽器初始會(huì)將頁(yè)面現(xiàn)在手機(jī)端顯示時(shí)進(jìn)行縮放,因此我們可以在meta標(biāo)簽中設(shè)置meta viewport屬性,告訴瀏覽器不將頁(yè)面進(jìn)行縮放,頁(yè)面寬度=用戶設(shè)備屏幕寬度
<meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no,maximum-scale=1.0,minimum-scale=1.0"/>/*頁(yè)面寬度=移動(dòng)寬度 :width=device-width用戶不可以縮放:user-scalable=no縮放比例:initial-scale=1最大縮放比例:maximum-scale=1.0最小縮放比例:minimum-scale=1.0*/
2.在移動(dòng)端幾乎使用的都是touch事件,與PC端不同
由于移動(dòng)端是觸摸事件,所以要用到H5的屬性touchstart/touchmove/touchend,但是PC端只支持鼠標(biāo)事件,所以要進(jìn)行特性檢測(cè)。
在 touch
事件里,是通過(guò) .touches[0].clientX
和 .touches[0].clientY
來(lái)獲取坐標(biāo)的,這點(diǎn)要和 mouse
事件區(qū)別開(kāi)。
問(wèn)題5:出現(xiàn)一個(gè)問(wèn)題就是清空之后,重新畫,然后出現(xiàn)原來(lái)的畫的東西
這個(gè)嘛,問(wèn)題不大,只不過(guò)是我漏寫context.beginPath(); ,也花了一點(diǎn)時(shí)間在上面解決bug,讓我想起“代碼千萬(wàn)行,注釋第一行;編程不規(guī)范,同事兩行淚 ”,還是按照文檔操作規(guī)范操作好,真香!!!
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持VeVb武林網(wǎng)。
新聞熱點(diǎn)
疑難解答