canvas進(jìn)階——實(shí)現(xiàn)連續(xù)平滑的曲線
關(guān)注并將「趣談前端」設(shè)為星標(biāo)
每早08:30按時推送技術(shù)干貨/優(yōu)秀開源/技術(shù)思維
前言
canvas真是個強(qiáng)大的東西,每天沉迷這個無法自拔, 可以做游戲,可以對圖片處理,后面會給大家分享一篇,canvas實(shí)現(xiàn)兩張圖片找不同的功能, 聽著是不是挺有意思的, 有點(diǎn)像游戲 「找你妹」,但是這都不是本篇文章想要表達(dá)的重點(diǎn),讀完今天這篇文章,你可以學(xué)到什么呢
「Canvas」 實(shí)現(xiàn)一個簡單的畫版小工具 「Canvas」 畫出平滑的曲線, 這是本篇文章的重點(diǎn)
這時候有人問我她??, 我的心里沒有她的,只有你們「coder」, 下面一起學(xué)習(xí)吧,預(yù)計(jì)閱讀5分鐘。
canvas實(shí)現(xiàn)一個畫版小工具
因?yàn)橐脖容^簡單,我大概說下思路:
首先我對canvas 畫布堅(jiān)監(jiān)聽3個事件, 分別是「mouseMove,mouseDown,mouseUp」 三個事件, 同時創(chuàng)建了isDown 這個變量, 用來標(biāo)記當(dāng)前畫圖是不是開啟 當(dāng)我們按下鼠標(biāo) 也就是「mouseDown」 事件, 表示開始畫筆,有一個初始的點(diǎn), 并把「isDown」 設(shè)置為「true」, 然后緊著呢開始移動, 可以確定直線的端點(diǎn), 然后再把直線的端點(diǎn)設(shè)置為下一條直線的起始點(diǎn), 不斷地重復(fù)這個過程, 「mousueUp」 將「isDown」 這個變量設(shè)置為「false」, 同時清空開始點(diǎn)和結(jié)束點(diǎn) 通過「mouseMove」事件不斷采集鼠標(biāo)經(jīng)過的坐標(biāo)點(diǎn),當(dāng)且僅當(dāng)「isDown」 為「true」(即處于書寫狀態(tài))時將當(dāng)前的點(diǎn)通過「canvas」的「LineTo」方法與前面的點(diǎn)進(jìn)行連接、繪制;
代碼如下:
class board {
constructor() {
this.canvas = document.getElementById('canvas')
this.canvas.addEventListener('mousemove', this.move.bind(this))
this.canvas.addEventListener('mousedown', this.down.bind(this))
this.canvas.addEventListener('mouseup', this.up.bind(this))
this.ctx = this.canvas.getContext('2d')
this.startP = null
this.endP = null
this.isDown = false
this.setLineStyle()
}
setLineStyle() {
this.ctx.strokeStyle = 'red'
this.ctx.lineWidth = 1
this.ctx.lineJoin = 'round'
this.ctx.lineCap = 'round'
}
move(e) {
if (!this.isDown) {
return
}
this.endP = this.getPot(e)
this.drawLine()
this.startP = this.endP
}
down(e) {
this.isDown = true
this.startP = this.getPot(e)
}
getPot(e) {
return new Point2d(e.offsetX, e.offsetY)
}
drawLine() {
if (!this.startP || !this.endP) {
return
}
this.ctx.beginPath()
this.ctx.moveTo(this.startP.x, this.startP.y)
this.ctx.lineTo(this.endP.x, this.endP.y)
this.ctx.stroke()
this.ctx.closePath()
}
up(e) {
this.startP = null
this.endP = null
this.isDown = false
}
}
new board()
point2d是我自己寫的一個2d點(diǎn)的一個類,不清楚的同學(xué)可以看我前幾篇文章, 這里就不重復(fù)闡述了。我們看下gif:

細(xì)心的同學(xué)可能發(fā)現(xiàn),畫的線折線感比較強(qiáng),出現(xiàn)這個本質(zhì)的原因—— 「就是我們畫出的線其實(shí)是一個多段線polyline, 連接兩個點(diǎn)之間的線是直線」
如何畫出平滑的曲線
想起曲線,就不得不提到貝塞爾曲線了,我之前的文章有系統(tǒng)的介紹過貝塞爾曲線,以及貝塞爾曲線方程的推導(dǎo)過程—— 傳送門
canvas 肯定是支持貝塞爾曲線的quadraticCurveTo(cp1x, cp1y, x, y) , 主要是一個起始點(diǎn), 一個終點(diǎn),一個控制點(diǎn)。其實(shí)這里可以用一個巧妙的算法去解決這樣的問題。
獲取二階貝塞爾曲線信息的算法
假設(shè)我們在鼠標(biāo)移動的過程中有A、B、C、D、E、F、G、這6個點(diǎn)。如何畫出平滑的曲線呢, 我們?nèi)點(diǎn)和C點(diǎn)的中點(diǎn)B1 作為第一條貝塞爾曲線的終點(diǎn),B點(diǎn)作為控制點(diǎn)。如圖:

接下來呢 算出 cd 的中點(diǎn) c1 以 B1 為起點(diǎn), c點(diǎn)為控制點(diǎn), c1為終點(diǎn)畫出下面圖形:

然后后面按照這樣的步驟不斷畫下去,就可以獲得平滑的曲線了。理論基礎(chǔ)我們明白了, 我們改造上面的畫線的方法:
實(shí)現(xiàn)畫出平滑的曲線
上面涉及到求兩個點(diǎn)的中間坐標(biāo):其實(shí)兩個坐標(biāo)的x 和y 分別除以2:代碼如下:
getMid(p1, p2) {
const x = (p1.x + p2.x) / 2
const y = (p1.y + p2.y) / 2
return new Point2d(x, y)
}
我們畫出二階貝塞爾曲線至少所示需要3個點(diǎn), 所以我們需要數(shù)組去存放移動過程中所有的點(diǎn)的信息。
我先實(shí)現(xiàn)畫貝塞爾曲線的方法:
drawCurve(controlP, endP) {
this.ctx.beginPath()
this.ctx.moveTo(this.startP.x, this.startP.y)
this.ctx.quadraticCurveTo(controlP.x, controlP.y, endP.x, endP.y)
this.ctx.stroke()
this.ctx.closePath()
}
然后在修改move 中的事件
move(e) {
if (!this.isDown) {
return
}
this.endP = this.getPot(e)
this.points.push(this.endP)
if (this.points.length >= 3) {
const [controlP, endP] = this.points.slice(-2)
const middle = this.getMid(controlP, endP)
this.drawCurve(controlP, middle)
this.startP = middle
}
}
這里實(shí)現(xiàn)永遠(yuǎn)取倒數(shù)后兩個點(diǎn),然后畫完貝塞爾曲線后再將 這個貝塞爾的終點(diǎn)設(shè)置為開始點(diǎn)方便下次畫。這樣是能保證畫出連續(xù)的貝塞爾曲線的。
我們看下gif 圖:

總結(jié)
至此本篇文章也算是寫完了, 如果你有更好的思路歡迎和我交流,我這只是粗略的表示。canvas畫連續(xù)平滑的曲線重點(diǎn)——還是怎么去找控制點(diǎn)這一點(diǎn)非常的重要哈!下一篇文章預(yù)告:canvas的離屏渲染和webworker的使用。
?? 看完三件事
如果你覺得這篇內(nèi)容對你挺有啟發(fā),我想邀請你幫我三個小忙:
點(diǎn)個【在看】,或者分享轉(zhuǎn)發(fā),讓更多的人也能看到這篇內(nèi)容 關(guān)注公眾號【趣談前端】,定期分享 工程化 / 可視化 / 低代碼 / 優(yōu)秀開源。

點(diǎn)個在看你最好看

