爲了讓她10分鐘入門canvas,我熬夜寫了3個小項目和這篇文章

「本文已參與好文召集令活動,點擊查看:後端、大前端雙賽道投稿,2萬元獎池等你挑戰!javascript

前言

你們好,我是林三心,回想起我當年校招的時候啊,屢次被面試官問到canvas,可是我卻不會,後來一直想找個機會學一下canvas,可是一直沒時間。canvas在前端的地位是愈來愈重要了,爲此,我特意寫了3個小項目,讓大家10分鐘就能入門canvas,是的,個人內心沒有她,只有大家css

image.png

1. canvas實現時鐘轉動

實現如下效果,分爲幾步:html

  • 一、找到canvas的中心,畫出表心,以及表框
  • 二、獲取當前時間,並根據時間畫出時針,分針,秒針,還有刻度
  • 三、使用定時器,每過一秒獲取新的時間,並從新繪圖,達到時鐘轉動的效果

截屏2021-07-19 下午8.52.15.png

1.1 表心,表框

畫表心,表框有兩個知識點:前端

  • 一、找到canvas的中心位置
  • 二、繪製圓形
//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() 
複製代碼

讓咱們來看看效果,發現了,好像不對啊,咱們是想畫兩個獨立的圓線,怎麼畫出來的兩個圓連到一塊兒了vue

截屏2021-07-19 下午9.10.07.png 緣由是:上面代碼畫連個圓時,是連着畫的,因此畫完大圓後,線還沒斬斷,就接着畫小圓,那確定會大圓小圓連一塊兒,解決辦法就是:beginPath,closePathjava

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 時針,分針,秒針

畫這三個指針,有兩個知識點:面試

  • 一、根據當前時,分,秒計算角度
  • 二、在計算好的角度上去畫出時針,分針,秒針

如何根據算好的角度去畫線呢,好比算出當前是3點,那麼時針就應該以12點爲起始點,順時針旋轉2 * Math.PI / 12 * 3 = 90°,分針和秒針也是一樣的道理,只不過跟時針不一樣的是比例問題而已,由於時在表上有12份,而分針和秒針都是60份canvas

截屏2021-07-19 下午10.07.19.png

這時候又有一個新問題,仍是以上面的例子爲例,我算出了90°,那咱們怎麼畫出時針呢?咱們可使用moveTo和lineTo去畫線段。至於90°,咱們只須要將x軸順時針旋轉90°,而後再畫出這條線段,那就獲得了指定角度的指針了。可是上面說了,是要以12點爲起始點,咱們的默認x軸確是水平的,因此咱們時分秒針算出角度後,每次都要減去90°。可能這有點繞,咱們經過下面的圖演示一下,仍是以上面3點的例子:後端

截屏2021-07-19 下午10.30.23.png

截屏2021-07-19 下午10.31.02.png 這樣就得出了3點指針的畫線角度了。markdown

又又又有新問題了,好比如今我畫完了時針,而後我想畫分針,x軸已經在我畫時針的時候偏轉了,這時候確定要讓x軸恢復到原來的模樣,咱們才能繼續畫分針,不然畫出來的分針是不許的。這時候save和restore就派上用場了,save是把ctx當前的狀態打包壓入棧中,restore是取出棧頂的狀態並賦值給ctxsave可屢次,可是restore取狀態的次數必須等於save次數

截屏2021-07-19 下午10.42.06.png

懂得了上面所說,剩下畫刻度了,起始刻度的道理跟時分秒針道理同樣,只不過刻度是死的,不須要計算,只須要規則畫出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 = new Date()
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.strokeStyle = '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.strokeStyle = '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.strokeStyle = '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()
複製代碼

截屏2021-07-19 下午10.53.53.png

最後一步就是更新視圖,使時鐘轉動起來,第一想到的確定是定時器setInterval,可是注意一個問題:每次更新視圖的時候都要把上一次的畫布清除,再開始畫新的視圖,否則就會出現千手觀音的景象

截屏2021-07-19 下午10.57.05.png

附上最終代碼:

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 = new Date()
    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.strokeStyle = '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.strokeStyle = '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.strokeStyle = '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.strokeStyle = 'red'
        ctx.stroke()
        ctx.closePath()
    }

    ctx.restore()
    ctx.restore()
}, 1000)
複製代碼

效果 very good啊:

clock的副本.gif

2. canvas實現刮刮卡

小時候不少人都買過充值卡把,懂的都懂啊哈,用指甲刮開這層灰皮,就能看底下的答案了。 截屏2021-07-19 下午11.02.09.png

思路是這樣的:

  • 一、底下答案是一個div,頂部灰皮是一個canvascanvas一開始蓋住div
  • 二、鼠標事件,點擊時並移動時,鼠標通過的路徑都畫圓形開路,而且設置globalCompositeOperationdestination-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.fillStyle = 'darkgray'
// 填充矩形 fillRect(起始X,起始Y,終點X,終點Y)
ctx.fillRect(0, 0, 400, 100)
ctx.fillStyle = '#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
}
複製代碼

效果以下:

guaguaka.gif

3. canvas實現畫板和保存

框架:使用vue + elementUI

其實很簡單,難點有如下幾點:

  • 一、鼠標拖拽畫正方形和圓形
  • 二、畫完一個保存畫布,下次再畫的時候疊加
  • 三、保存圖片

第一點,只須要計算出鼠標點擊的點座標,以及鼠標的當前座標,就能夠計算了,矩形長寬計算:x - beginX, y - beginY,圓形則要利用勾股定理:Math.sqrt((x - beginX) * (x - beginX) + (y - beginY) * (y - beginY))

第二點,則要利用canvas的getImageDataputImageData方法

第三點,思路是將canvas生成圖片連接,並賦值給具備下載功能的a標籤,並主動點擊a標籤進行圖片下載

看看效果吧:

截屏2021-07-19 下午11.16.24.png

截屏2021-07-19 下午11.17.41.png

具體代碼我就不過多講解了,說難也不難,只要前面兩個項目理解了,這個項目很容易就懂了:

<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-picker v-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> export default { 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.fillStyle = 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.strokeStyle = 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.strokeStyle = 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>

<style lang="scss" scoped> #canvas { border: 1px solid black; } </style>
複製代碼

結語

學習羣請點這裏

相關文章
相關標籤/搜索