前言
本文主要介紹:
- 專案介紹
- 專案效果展示
- 一步步實現專案效果
- 踩坑
一、專案介紹
名稱: 智繪畫板
技術棧: HTML5,CSS3,JavaScript,移動端
功能描述:
- 支援PC端和移動端線上繪畫功能
- 實現任意選擇畫筆顏色、調整畫筆粗細以及橡皮檫擦除等繪畫功能
- 實現線上畫板的本地儲存功能
- 支援撤銷和返回操作
自定義背景顏色[這個功能尚未完善好] 注:本專案僅僅是canvas和JavaScript練手小專案,存在一個問題尚未解決,橡皮擦把背景層都給擦掉了,希望有大神給一些建議給我,謝謝!
二、專案效果展示
預覽圖
PC端的預覽圖:
移動端的預覽圖:
看完上面的預覽圖和體驗過智繪畫板覺得還可以的,記得點個贊哦,不管你是否十分激動,反正我是挺激動的,畢竟自己實現出現的專案效果,挺自豪的,說了一堆廢話,下面就可以動起手來敲程式碼,實現自己想要的效果!!!
注:下面實現專案效果主要是關於JavaScript方面的,下面僅僅是提供實現思路的程式碼,並非全部程式碼。
三、一步步實現專案效果
(一)分析頁面
通過用例圖,我們知道使用者進入我們這個網站有哪些功能?
使用者可以進行的操作:
- 畫畫
- 改變畫筆的粗細
- 切換畫筆的顏色
- 使用橡皮檫擦除不想要的部分
- 清空畫板
- 將自己畫的東西儲存成圖片
- 進行撤銷和重做操作
切換畫板背景顏色- 相容移動端(支援觸控)
(二)進行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()
方法把記錄的資料畫出來。
- 初始化當前畫板的畫筆狀態,
painting = false
。 - 當滑鼠按下時(
mousedown
),把painting
設為true
,表示正在畫,滑鼠沒鬆開。把滑鼠點記錄下來。 - 當按下滑鼠的時候,滑鼠移動(
mousemove
)就把點記錄下來並畫出來。 - 如果滑鼠移動過快,瀏覽器跟不上繪畫速度,點與點之間會出現間隙,所以我們需要將畫出的點用線連起來(
lineTo()
)。 - 滑鼠鬆開的時候(
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.實現橡皮擦功能
實現思路:
- 獲取橡皮擦元素
- 設定橡皮擦初始狀態,
eraserEnabled = false
。 - 監聽橡皮擦
click
事件,點選橡皮擦,改變橡皮擦狀態,eraserEnabled = true
,並且切換class,實現被啟用的效果。 eraserEnabled
為true
,移動滑鼠用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.實現清屏功能
實現思路:
- 獲取元素節點。
- 點選清空按鈕清空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.實現儲存成圖片功能
實現思路:
- 獲取
canvas.toDateURL
- 在頁面裡建立並插入一個a標籤
- a標籤href等於
canvas.toDateURL
,並新增download屬性 - 點選儲存按鈕,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.實現改變背景顏色的功能
實現思路:
獲取相應的元素節點。給每一個class為bgcolor-item的標籤新增點選事件,當點選事件觸發時,改變背景顏色。點選設定背景顏色的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.實現改變畫筆粗細的功能
實現思路:
- 實現讓設定畫筆的屬性的對話方塊出現。
- 獲取相應的元素節點。
- 當input=range的元素髮生改變的時候,獲取到的值賦值給lWidth。
- 然後設定
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.實現改變撤銷和重做的功能
實現思路:
- 儲存快照:每完成一次繪製操作則儲存一份 canvas 快照到
canvasHistory
陣列(生成快照使用 canvas 的toDataURL()
方法,生成的是 base64 的圖片); - 撤銷和反撤銷:把
canvasHistory
陣列中對應索引的快照使用 canvas 的drawImage()
方法重繪一遍; - 繪製新影象:執行新的繪製操作時,刪除當前位置之後的陣列記錄,然後新增新的快照。
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.相容移動端
實現思路:
- 判斷裝置是否支援觸控
true
,則使用touch
事件;false
,則使用mouse
事件
// ...
if (document.body.ontouchstart !== undefined) {
// 使用touch事件
anvas.ontouchstart = function (e) {
// 開始觸控
}
canvas.ontouchmove = function (e) {
// 開始滑動
}
canvas.ontouchend = function () {
// 滑動結束
}
}else{
// 使用mouse事件
// ...
}
// ...
複製程式碼
四、踩坑
問題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、提需求也是可以的
覺得對您有幫助的話就點個贊或收藏吧!
歡迎轉載或分享,轉載時請註明出處