vue + canvas實現(xiàn)涂鴉面板的示例代碼
此篇文章用于記錄柏成從零開發(fā)一個canvas涂鴉面板的歷程,最終效果如下:

介紹
我們基于 canvas 實現(xiàn)了一款簡單的涂鴉面板,用于在網(wǎng)頁上進(jìn)行繪圖和創(chuàng)作。其支持以下快捷鍵:
| 功能 | 快捷鍵 |
|---|---|
| 撤銷 | Ctrl + Z |
| 恢復(fù) | Ctrl + Y |
我們可以通過 new Board 創(chuàng)建一個空白畫板,其接收一個容器作為參數(shù),下面是個基本例子:
<template>
<div class="drawing-board">
<div id="container" ref="container" style="width: 100%; height: 100%"></div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Board from '@/canvas/board.js'
const container = ref(null)
onMounted(() => {
// 創(chuàng)建一個空白畫板
new Board(container.value)
})
</script>初始化
Board 的實現(xiàn)是一個類,在 src/canvas/board.js中定義。
new Board(container)時做了什么?我們在構(gòu)造函數(shù)中創(chuàng)建一個 canvas 畫布追加到了 container 容器中,并定義了一系列屬性,最后執(zhí)行了 init 初始化方法。
在初始化方法中,我們設(shè)置了畫筆樣式(其實可以動態(tài)去設(shè)置,讓用戶選擇畫筆顏色、粗細(xì)、線條樣式等,時間有限,未實現(xiàn)此功能);注冊監(jiān)聽了鼠標(biāo)鍵盤事件,用于繪制畫筆軌跡和實現(xiàn)撤銷恢復(fù)快捷鍵操作。
export default class BoardCanvas {
constructor(container) {
// 容器
this.container = container
// canvas畫布
this.canvas = this.createCanvas(container)
// 繪制工具
this.ctx = this.canvas.getContext('2d')
// 起始點(diǎn)位置
this.startX = 0
this.stateY = 0
// 畫布?xì)v史棧
this.pathSegmentHistory = []
this.index = 0
// 初始化
this.init()
}
// 創(chuàng)建畫布
createCanvas(container) {
const canvas = document.createElement('canvas')
canvas.width = container.clientWidth
canvas.height = container.clientHeight
canvas.style.display = 'block'
canvas.style.backgroundColor = 'antiquewhite'
container.appendChild(canvas)
return canvas
}
// 初始化
init() {
this.addPathSegment()
this.setContext2DStyle()
// 阻止默認(rèn)右擊事件
this.canvas.addEventListener('contextmenu', (e) => e.preventDefault())
// 自定義鼠標(biāo)按下事件
this.canvas.addEventListener('mousedown', this.mousedownEvent.bind(this))
// 自定義鍵盤按下事件
window.document.addEventListener('keydown', this.keydownEvent.bind(this))
}
// 設(shè)置畫筆樣式
setContext2DStyle() {
this.ctx.strokeStyle = '#EB7347'
this.ctx.lineWidth = 3
this.ctx.lineCap = 'round'
this.ctx.lineJoin = 'round'
}
}自定義鼠標(biāo)事件
我們之前在 init 初始化方法中注冊了 onmousedown 鼠標(biāo)按下事件,需要在此處實現(xiàn)鼠標(biāo)按下拖拽可以繪制畫筆軌跡的邏輯
mousedownEvent(e) {
const that = this
const ctx = this.ctx
ctx.beginPath()
ctx.moveTo(e.offsetX, e.offsetY)
ctx.stroke()
this.canvas.onmousemove = function (e) {
ctx.lineTo(e.offsetX, e.offsetY)
ctx.stroke()
}
this.canvas.onmouseup = this.canvas.onmouseout = function () {
that.addPathSegment()
this.onmousemove = null
this.onmouseup = null
this.onmouseout = null
}
}自定義鍵盤事件
我們之前在 init 初始化方法中注冊了 onkeydown 鍵盤按下事件,需要在此處實現(xiàn)撤銷恢復(fù)的邏輯
// 鍵盤事件
keydownEvent(e) {
if (!e.ctrlKey) return
switch (e.keyCode) {
case 90:
this.undo()
break
case 89:
this.redo()
break
}
}要實現(xiàn)撤銷恢復(fù)操作,我們需要一個存儲畫布快照的棧!這又涉及到兩個問題,我們?nèi)绾潍@取到當(dāng)前畫布快照?如何根據(jù)快照數(shù)據(jù)恢復(fù)畫布?
查閱 canvas官方API文檔 得知,獲取快照 API 為 getImageData;通過快照恢復(fù)畫布的 API 為 putImageData
/*
* @name 返回一個 ImageData 對象,其中包含 Canvas 畫布部分或完整的像素點(diǎn)信息
* @param { Number } sx 將要被提取的圖像數(shù)據(jù)矩形區(qū)域的左上角 x 坐標(biāo)
* @param { Number } sy 將要被提取的圖像數(shù)據(jù)矩形區(qū)域的左上角 y 坐標(biāo)
* @param { Number } sWidth 將要被提取的圖像數(shù)據(jù)矩形區(qū)域的寬度
* @param { Number } sHeight 將要被提取的圖像數(shù)據(jù)矩形區(qū)域的高度
* @return { Object } 返回一個 ImageData 對象,包含 Canvas 給定的矩形圖像像素點(diǎn)信息
*/
context.getImageData(sx, sy, sWidth, sHeight);
/*
* @name 將給定 ImageData 對象的數(shù)據(jù)繪制到位圖上
* @param { Object } ImageData 對象,包含 Canvas 給定的矩形圖像像素點(diǎn)信息
* @param { Number } dx 目標(biāo) Canvas 中被圖像數(shù)據(jù)替換的起點(diǎn)橫坐標(biāo)
* @param { Number } dy 目標(biāo) Canvas 中被圖像數(shù)據(jù)替換的起點(diǎn)縱坐標(biāo)
*/
context.putImageData(ImageData, dx, dy);我們對保存畫布快照的邏輯進(jìn)行了一次封裝,如下:
// 添加路徑片段
addPathSegment() {
const data = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height)
// 刪除當(dāng)前索引后的路徑片段,然后追加一個新的路徑片段,更新索引
this.pathSegmentHistory.splice(this.index + 1)
this.pathSegmentHistory.push(data)
this.index = this.pathSegmentHistory.length - 1
}我們在構(gòu)造函數(shù)中定義了一個存儲畫布快照的棧 - pathSegmentHistory;一個指向棧中當(dāng)前快照的索引 - index
在初始化和繪制一個路徑片段結(jié)束時都會調(diào)用 addPathSegment 方法,用于保存當(dāng)前畫布快照到棧中,并將索引指向棧中的最后一個成員
Tip:在保存快照數(shù)據(jù)之前,我們會先刪除棧中位于索引之后的全部快照數(shù)據(jù),目的是執(zhí)行撤銷操作后再繪制軌跡,要清空棧中的多余數(shù)據(jù)。舉個栗子,如果我們先執(zhí)行3次undo,再執(zhí)行一次redo,最后繪制一條新的軌跡,則需要先清除棧中的最后兩條快照數(shù)據(jù),再添加一條新的當(dāng)前畫布快照數(shù)據(jù),示意圖如下

撤銷(undo)
當(dāng)執(zhí)行 undo 操作時,我們先將索引前移, 然后取出當(dāng)前索引指向的快照數(shù)據(jù),重新繪制畫布
// 撤銷
undo() {
if (this.index <= 0) return
this.index--
this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)
}
恢復(fù)(redo)
當(dāng)執(zhí)行 redo 操作時,我們先將索引后移, 然后取出當(dāng)前索引指向的快照數(shù)據(jù),重新繪制畫布
// 恢復(fù)
redo() {
if (this.index >= this.pathSegmentHistory.length - 1) return
this.index++
this.ctx.putImageData(this.pathSegmentHistory[this.index], 0, 0)
}
源碼
涂鴉面板demo代碼:vue-canvas
到此這篇關(guān)于vue + canvas實現(xiàn)涂鴉面板的示例代碼的文章就介紹到這了,更多相關(guān)vue + canvas涂鴉面板內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
一步步教你利用webpack如何搭一個vue腳手架(超詳細(xì)講解和注釋)
這篇文章主要給大家介紹了軟玉利用webpack如何搭一個vue腳手架的相關(guān)資料,文中有超詳細(xì)講解和注釋,對大家學(xué)習(xí)或者使用webpack具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-01-01
vue基于better-scroll實現(xiàn)左右聯(lián)動滑動頁面
這篇文章主要介紹了vue基于better-scroll實現(xiàn)左右聯(lián)動滑動頁面,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-06-06
詳解vue-cli本地環(huán)境API代理設(shè)置和解決跨域
這篇文章主要介紹了詳解vue-cli本地環(huán)境API代理設(shè)置和解決跨域,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-09-09
詳解基于Vue2.0實現(xiàn)的移動端彈窗(Alert, Confirm, Toast)組件
這篇文章主要介紹了詳解基于Vue2.0實現(xiàn)的移動端彈窗(Alert, Confirm, Toast)組件,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08
vue3+element 分片上傳與分片下載功能實現(xiàn)方法詳解
這篇文章主要介紹了vue3+element 分片上傳與分片下載功能實現(xiàn)方法,結(jié)合實例形式詳細(xì)分析了vue3+element 分片上傳與下載相關(guān)實現(xiàn)技巧與操作注意事項,需要的朋友可以參考下2023-06-06

