vue + canvas實(shí)現(xiàn)涂鴉面板的示例代碼
此篇文章用于記錄柏成從零開發(fā)一個canvas涂鴉面板
的歷程,最終效果如下:
介紹
我們基于 canvas 實(shí)現(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 的實(shí)現(xiàn)是一個類,在 src/canvas/board.js
中定義。
new Board(container)
時做了什么?我們在構(gòu)造函數(shù)中創(chuàng)建一個 canvas 畫布追加到了 container 容器中,并定義了一系列屬性,最后執(zhí)行了 init 初始化方法。
在初始化方法中,我們設(shè)置了畫筆樣式(其實(shí)可以動態(tài)去設(shè)置,讓用戶選擇畫筆顏色、粗細(xì)、線條樣式等,時間有限,未實(shí)現(xiàn)此功能);注冊監(jiān)聽了鼠標(biāo)鍵盤事件,用于繪制畫筆軌跡和實(shí)現(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)按下事件,需要在此處實(shí)現(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 鍵盤按下事件,需要在此處實(shí)現(xiàn)撤銷恢復(fù)的邏輯
// 鍵盤事件 keydownEvent(e) { if (!e.ctrlKey) return switch (e.keyCode) { case 90: this.undo() break case 89: this.redo() break } }
要實(shí)現(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實(shí)現(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-01vue基于better-scroll實(shí)現(xiàn)左右聯(lián)動滑動頁面
這篇文章主要介紹了vue基于better-scroll實(shí)現(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實(shí)現(xiàn)的移動端彈窗(Alert, Confirm, Toast)組件
這篇文章主要介紹了詳解基于Vue2.0實(shí)現(xiàn)的移動端彈窗(Alert, Confirm, Toast)組件,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-08-08vue3+element 分片上傳與分片下載功能實(shí)現(xiàn)方法詳解
這篇文章主要介紹了vue3+element 分片上傳與分片下載功能實(shí)現(xiàn)方法,結(jié)合實(shí)例形式詳細(xì)分析了vue3+element 分片上傳與下載相關(guān)實(shí)現(xiàn)技巧與操作注意事項(xiàng),需要的朋友可以參考下2023-06-06