前言
大家好,我是林三心,回想起我當年校招的時候啊,多次被面試官問到
canvas
,但是我卻不會,後來一直想找個機會學一下
canvas
,但是一直沒時間。
canvas
在前端的地位是越來越重要了,為此,我特地寫了3個小計畫,讓你們10分鐘就能入門
canvas
,是的,我的心裏沒有她,只有你們
1. canvas實作時鐘轉動
實作以下效果,分為幾步:
1、找到canvas的
中心
,畫出
表心
,以及
表框
2、獲取
當前時間
,並根據時間畫出
時針,分針,秒針
,還有
刻度
3、使用定時器,每過一秒
獲取新的時間
,並重新繪圖,達到時鐘
轉動的效果
1.1 表心,表框
畫表心,表框有兩個知識點:
1、找到canvas的
中心位置
2、繪制
圓形
//html
<canvas id="canvas" width="600" height="600"></canvas>
// js
// 設定中心點,此時300,300變成了座標的0,0
ctx.translate(300, 300)
// 畫圓線使用arc(中心點X,中心點Y,半徑,起始角度,結束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
// 執行畫線段的操作stroke
ctx.stroke()
讓我們來看看效果,發現了,好像不對啊,我們是想畫
兩個獨立的圓線
,怎麽畫出來的兩個圓
連到一起了
:
beginPath,closePath
ctx.translate(300, 300) // 設定中心點,此時300,300變成了座標的0,0
// 畫大圓
+ ctx.beginPath()
// 畫圓線使用arc(中心點X,中心點Y,半徑,起始角度,結束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 執行畫線段的操作
+ ctx.closePath()
// 畫小圓
+ ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
+ ctx.closePath()
1.2 時針,分針,秒針
畫這三個指標,有兩個知識點:
1、根據當前
時,分,秒
去
計算角度
2、在計算好的角度上去畫出
時針,分針,秒針
如何根據算好的角度去畫線呢,比如算出當前是
3點
,那麽時針就應該以
12點
為起始點,
順時針
旋轉
2 * Math.PI / 12 * 3 = 90°
,分針和秒針也是同樣的道理,只不過跟時針不同的是
比例問題
而已,因為
時在表上有12份
,而
分針和秒針都是60份
這時候又有一個新問題,還是以上面的例子為例,我算出了
90°
,那我們怎麽畫出時針呢?我們可以使用
moveTo和lineTo
去畫線段。至於90°,我們只需要將
x軸
順時針旋轉
90°
,然後再畫出這條線段,那就得到了指定角度的指標了。但是上面說了,是要以
12點為起始點
,我們的
預設x軸確是水平
的,所以我們分時秒針算出角度後,每次都要
減去90°
。可能這有點繞,我們透過下面的圖演示一下,還是以上面
3點
的例子:
又又又有新問題了,比如現在我畫完了時針,然後我想畫分針,x軸已經在我畫時針的時候偏轉了,這時候肯定要讓x軸恢復到原來的模樣,我們才能繼續畫分針,否則畫出來的分針是不準的。這時候
save和restore
就派上用場了,
save是把ctx當前的狀態打包壓入棧中,restore是取出棧頂的狀態並賦值給ctx
,
save可多次,但是restore取狀態的次數必須等於save次數
懂得了上面所說,剩下畫
刻度
了,起始刻度的道理跟分時秒針道理一樣,只不過刻度是死的,不需要計算,只需要規則畫出
60個小刻度
,和
12個大刻度
就行
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
ctx.translate(300, 300) // 設定中心點,此時300,300變成了座標的0,0
// 把狀態保存起來
+ ctx.save()
// 畫大圓
ctx.beginPath()
// 畫圓線使用arc(中心點X,中心點Y,半徑,起始角度,結束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 執行畫線段的操作
ctx.closePath()
// 畫小圓
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()
----- 新加程式碼 ------
// 獲取當前 時,分,秒
let time = newDate()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()
// 時針
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
ctx.beginPath()
// moveTo設定畫線起點
ctx.moveTo(-10, 0)
// lineTo設定畫線經過點
ctx.lineTo(40, 0)
// 設定線寬
ctx.lineWidth = 10
ctx.stroke()
ctx.closePath()
// 恢復成上一次save的狀態
ctx.restore()
// 恢復完再保存一次
ctx.save()
// 分針
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.lineWidth = 5
ctx.stroke style = 'blue'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()
//秒針
ctx.rotate(2 * Math.PI / 60 * sec - - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.stroke style = 'red'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()
// 繪制刻度,也是跟繪制分時秒針一樣,只不過刻度是死的
ctx.lineWidth = 1
for (let i = 0; i < 60; i++) {
ctx.rotate(2 * Math.PI / 60)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
// ctx.stroke style = 'red'
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 5
for (let i = 0; i < 12; i++) {
ctx.rotate(2 * Math.PI / 12)
ctx.beginPath()
ctx.moveTo(85, 0)
ctx.lineTo(100, 0)
ctx.stroke()
ctx.closePath()
}
ctx.restore()
最後一步就是更新檢視,使時鐘轉動起來,第一想到的肯定是定時器
setInterval
,但是註意一個問題:每次更新檢視的時候都要把上一次的畫布清除,再開始畫新的檢視,不然就會出現
千手觀音
的景象
附上最終程式碼:
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
setInterval(() => {
ctx.save()
ctx.clearRect(0, 0, 600, 600)
ctx.translate(300, 300) // 設定中心點,此時300,300變成了座標的0,0
ctx.save()
// 畫大圓
ctx.beginPath()
// 畫圓線使用arc(中心點X,中心點Y,半徑,起始角度,結束角度)
ctx.arc(0, 0, 100, 0, 2 * Math.PI)
ctx.stroke() // 執行畫線段的操作
ctx.closePath()
// 畫小圓
ctx.beginPath()
ctx.arc(0, 0, 5, 0, 2 * Math.PI)
ctx.stroke()
ctx.closePath()
// 獲取當前 時,分,秒
let time = newDate()
let hour = time.getHours() % 12
let min = time.getMinutes()
let sec = time.getSeconds()
// 時針
ctx.rotate(2 * Math.PI / 12 * hour + 2 * Math.PI / 12 * (min / 60) - Math.PI / 2)
ctx.beginPath()
// moveTo設定畫線起點
ctx.moveTo(-10, 0)
// lineTo設定畫線經過點
ctx.lineTo(40, 0)
// 設定線寬
ctx.lineWidth = 10
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()
// 分針
ctx.rotate(2 * Math.PI / 60 * min + 2 * Math.PI / 60 * (sec / 60) - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(60, 0)
ctx.lineWidth = 5
ctx.stroke style = 'blue'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()
//秒針
ctx.rotate(2 * Math.PI / 60 * sec - Math.PI / 2)
ctx.beginPath()
ctx.moveTo(-10, 0)
ctx.lineTo(80, 0)
ctx.stroke style = 'red'
ctx.stroke()
ctx.closePath()
ctx.restore()
ctx.save()
// 繪制刻度,也是跟繪制分時秒針一樣,只不過刻度是死的
ctx.lineWidth = 1
for (let i = 0; i < 60; i++) {
ctx.rotate(2 * Math.PI / 60)
ctx.beginPath()
ctx.moveTo(90, 0)
ctx.lineTo(100, 0)
// ctx.stroke style = 'red'
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.save()
ctx.lineWidth = 5
for (let i = 0; i < 12; i++) {
ctx.rotate(2 * Math.PI / 12)
ctx.beginPath()
ctx.moveTo(85, 0)
ctx.lineTo(100, 0)
// ctx.stroke style = 'red'
ctx.stroke()
ctx.closePath()
}
ctx.restore()
ctx.restore()
}, 1000)
效果
very good
啊:
2. canvas實作刮刮卡
小時候很多人都買過充值卡把,懂的都懂啊哈,用指甲刮開這層灰皮,就能看底下的答案了。
思路是這樣的:
1、底下答案是一個
div
,頂部灰皮是一個
canvas
,
canvas
一開始蓋住
div
2、滑鼠事件,點選時並移動時,滑鼠經過的路徑都
畫圓形
開路,並且設定
globalCompositeOperation
為
destination-out
,使滑鼠經過的路徑都
變成透明
,一透明,自然就顯示出下方的答案資訊。
關於
fill
這個方法,其實是對標
stroke
的,
fill
是把圖形填充,
stroke
只是畫出邊框線
// html
<canvas id="canvas" width="400" height="100"></canvas>
<div class="text">恭喜您獲得100w</div>
< style>
* {
margin: 0;
padding: 0;
}
.text {
position: absolute;
left: 130px;
top: 35px;
z-index: -1;
}
</ style>
// js
const canvas = document.getElementById('canvas')
const ctx = canvas.getContext('2d')
// 填充的顏色
ctx.fill style = 'darkgray'
// 填充矩形 fillRect(起始X,起始Y,終點X,終點Y)
ctx.fillRect(0, 0, 400, 100)
ctx.fill style = '#fff'
// 繪制填充文字
ctx.fillText('刮刮卡', 180, 50)
let isDraw = false
canvas.onmousedown = function () {
isDraw = true
}
canvas.onmousemove = function (e) {
if (!isDraw) return
// 計算滑鼠在canvas裏的位置
const x = e.pageX - canvas.offsetLeft
const y = e.pageY - canvas.offsetTop
// 設定globalCompositeOperation
ctx.globalCompositeOperation = 'destination-out'
// 畫圓
ctx.arc(x, y, 10, 0, 2 * Math.PI)
// 填充圓形
ctx.fill()
}
canvas.onmouseup = function () {
isDraw = false
}
效果如下:
3. canvas實作畫板和保存
框架:使用
vue + elementUI
其實很簡單,難點有以下幾點:
1、滑鼠拖拽畫正方形和圓形
2、畫完一個保存畫布,下次再畫的時候疊加
3、保存圖片
第一點,只需要計算出滑鼠點選的點座標,以及滑鼠的當前座標,就可以計算了,矩形長寬計算:
x - beginX, y - beginY
,圓形則要利用勾股定理:
Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
第二點,則要利用canvas的
getImageData
和
putImageData
方法
第三點,思路是將
canvas
生成圖片連結,並賦值給具有下載功能的
a標簽
,並主動點選
a標簽
進行
圖片下載
看看效果吧:
具體程式碼我就不過多講解了,說難也不難,只要前面兩個計畫理解了,這個計畫很容易就懂了:
<template>
<div>
<div style="margin-bottom: 10px; display: flex; align-items: center">
<el-button @click="changeType('huabi')"type="primary">畫筆</el-button>
<el-button @click="changeType('rect')"type="success">正方形</el-button>
<el-button
@click="changeType('arc')"
type="warning"
style="margin-right: 10px"
>圓形</el-button
>
<div>顏色:</div>
<el-color-pickerv-model="color"></el-color-picker>
<el-button @click="clear">清空</el-button>
<el-button @click="saveImg">保存</el-button>
</div>
<canvas
id="canvas"
width="800"
height="400"
@mousedown="canvasDown"
@mousemove="canvasMove"
@mouseout="canvasUp"
@mouseup="canvasUp"
>
</canvas>
</div>
</template>
<script>
exportdefault {
data() {
return {
type: "huabi",
isDraw: false,
canvasDom: null,
ctx: null,
beginX: 0,
beginY: 0,
color: "#000",
imageData: null,
};
},
mounted() {
this.canvasDom = document.getElementById("canvas");
this.ctx = this.canvasDom.getContext("2d");
},
methods: {
changeType(type) {
this.type = type;
},
canvasDown(e) {
this.isDraw = true;
const canvas = this.canvasDom;
this.beginX = e.pageX - canvas.offsetLeft;
this.beginY = e.pageY - canvas.offsetTop;
},
canvasMove(e) {
if (!this.isDraw) return;
const canvas = this.canvasDom;
const ctx = this.ctx;
const x = e.pageX - canvas.offsetLeft;
const y = e.pageY - canvas.offsetTop;
this[`${this.type}Fn`](ctx, x, y);
},
canvasUp() {
this.imageData = this.ctx.getImageData(0, 0, 800, 400);
this.isDraw = false;
},
huabiFn(ctx, x, y) {
ctx.beginPath();
ctx.arc(x, y, 5, 0, 2 * Math.PI);
ctx.fill style = this.color;
ctx.fill();
ctx.closePath();
},
rectFn(ctx, x, y) {
const beginX = this.beginX;
const beginY = this.beginY;
ctx.clearRect(0, 0, 800, 400);
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
ctx.beginPath();
ctx.stroke style = this.color;
ctx.rect(beginX, beginY, x - beginX, y - beginY);
ctx.stroke();
ctx.closePath();
},
arcFn(ctx, x, y) {
const beginX = this.beginX;
const beginY = this.beginY;
this.isDraw && ctx.clearRect(0, 0, 800, 400);
this.imageData && ctx.putImageData(this.imageData, 0, 0, 0, 0, 800, 400);
ctx.beginPath();
ctx.stroke style = this.color;
ctx.arc(
beginX,
beginY,
Math.round(
Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))
),
0,
2 * Math.PI
);
ctx.stroke();
ctx.closePath();
},
saveImg() {
const url = this.canvasDom.toDataURL();
const a = document.createElement("a");
a.download = "sunshine";
a.href = url;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
},
clear() {
this.imageData = null
this.ctx.clearRect(0, 0, 800, 400)
}
},
};
</script>
< stylelang="scss"scoped>
#canvas {
border: 1px solid black;
}
</ style>
我是林三心
一個待過 小型toG型外包公司、大型外包公司、小公司、潛力型創業公司、大公司 的作死型前端選手;
一個偏前端的全幹工程師;
一個不正經的金塊作者;
逗比的B站up主;
不帥的小紅書博主;
喜歡打鐵的籃球菜鳥;
喜歡歷史的乏味少年;
喜歡rap的五音不全弱雞如果你想一起學習前端,一起摸魚,一起研究簡歷最佳化,一起研究面試進步,一起交流歷史音樂籃球rap,可以來俺的摸魚學習群哈哈,點這個,有7000多名前端小夥伴在等著一起學習哦 --> 摸魚沸點
廣州的兄弟可以約飯哦,或者約球~