JS利用原生canvas實現(xiàn)圖形標注功能
由于工作需要,項目中要求實現(xiàn)一個功能—在視頻或者圖片上進行圖形標注,支持矩形、多邊形、線段、圓形、折線,已繪制的圖形可以進行縮放,移動。
完整功能源代碼在這個倉庫,感興趣的可以clone下來跑一下。
接下來我將實現(xiàn)一個dmeo來展示其中最簡單的圖形—矩形的創(chuàng)建。
初始化頁面
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> <style> body { margin: 0; padding: 0; height: 100vh; display: flex; align-items: center; justify-content: center; } canvas { border: 1px solid saddlebrown; background-color: beige; } </style> </head> <body> <canvas id="canvasIdChart" class="canvas">您的瀏覽器不支持canvas1元素</canvas> <script src="./drawClass.js"></script> <script> const chart = new Chart('canvasIdChart') </script> </body> </html>
該頁面只有一個canvas,而功能的實現(xiàn)在drawClass里面,這里我實現(xiàn)了一個Chart類,需要傳入一個canvas的id,表示后續(xù)的繪制功能都是在這個canvas上實現(xiàn)的。
創(chuàng)建基礎類Chart
首先梳理下要實現(xiàn)一個圖形標注需要涉及到什么事件
用戶在canvas上按下鼠標,說明用戶想要開始畫一個矩形進行標注了,所以canvas上需要綁定一個onmousedown
事件。相應的繪制過程也涉及到了onmousemove
事件,而用戶松開鼠標表示繪制已完成,即需要onmouseup
事件。同時為了兼容這樣一種情況—用戶在繪制過程中鼠標移出了canvas的范圍(此時的鼠標事件都是綁定在canvas上的),這種情況無法觸發(fā)canvas的onmouseup
事件,所以為canvas加上onmouseout
,鼠標移出默認代表繪制完成。
初始化代碼如下
class Chart { constructor(canvasId) { this.cvs = document.getElementById(canvasId) this.ctx = this.cvs.getContext('2d') this.shapes = [] // 保存圖形數據的數組 this.init() // 初始化canvas得寬高 this.bindEvent() // 為canvas綁定鼠標事件 this.draw() // canvas的繪制 this.isClickDown = false // 當前鼠標是否按下 this.currentShape = null // 當前選中的圖形 } init() { const w = 1000, h = 600 this.cvs.width = w this.cvs.height = h } bindEvent() { this.cvs.onmousedown = (e) => { this.isClickDown = true // 鼠標按下的時候創(chuàng)建其他鼠標事件 that.cvs.onmousemove = function (e){} that.cvs.onmouseup = function (e){} that.cvs.onmouseout = function (e){} } } draw() {} }
為了后續(xù)拓展,我將各種圖形封裝成一個個類,如矩形封裝成class **Rectangle
。這樣做的好處是我們只需要規(guī)定這樣的圖形類內部都有某些屬性和方法給Chart中的draw調用就行,比如我們可以把繪制矩形的實現(xiàn)寫在Rectangle
類的draw函數上,而在Chart中的draw上只需調用new Rectangle().draw()
即可。要注意的是,draw方法是隨著mousemove而要不斷的刷新canvas重新繪制的,此時最好使用requestAnimationFrame
方法讓瀏覽在合適的時機調用draw。此時Chart的draw方法如下:**
class Chart { .... draw() { requestAnimationFrame(this.draw.bind(this)) this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height) // 首先清空canvas // 將shapes里面的圖形重新繪制 for (const s of this.shapes) { s.draw() } } }
實現(xiàn)Rectangle類
首先看一下矩形需要的幾點要素
- 填充的顏色
- 矩形的初始位置(即鼠標按下的位置)
- 矩形的終點位置(即鼠標抬起的位置,也是鼠標移動的最后位置)
- 矩形需要繪制在哪里(此時需要繪制在canvas上)
- 矩形的初始位置可能會大于終點位置(鼠標按下后往左上角繪制),此時可用class語法中的get方法獲取minX、maxX、minY、maxY
- 因為同類幾何圖形的實例可能會有很多,所以每個實例需要一個id
初始代碼如下:
/** * 生成唯一ID * @param {Number} length 生成id的長度 * @returns */ const genID = (length = 3) => { return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36) } class Rectangle { constructor(el,color, startX, startY) { this.el = el // 保存需要繪制的載體 this.shapeType = 'RECT' // 當前幾何圖形的類別 this.color = color // 填充的顏色 // 初始化起點,默認起點和終點一樣 this.startX = startX this.startY = startY this.endX = startX this.endY = startY this.action = 'CREATE' // 當前的操作類型 this.id = genID() // 當前實例的唯一id } get minX() { return Math.min(this.startX, this.endX) } get maxX() { return Math.max(this.startX, this.endX) } get minY() { return Math.min(this.startY, this.endY) } get maxY() { return Math.max(this.startY, this.endY) } draw() {} }
實現(xiàn)Rectangle類的draw函數 幸運的是canvas得API中已經有了繪制矩形的方法,不用我們一條線一條線的自己畫了。該方法是 canvas.rect(startX,startY,width,height)
四個參數分別是矩形的起點橫坐標和縱坐標,矩形的寬高,這也是為什么需要minX和minY。代碼如下:
class Rectangle { ... draw() { this.el.beginPath() // 畫筆起始點 this.el.rect( this.minX, this.minY, (this.maxX - this.minX), (this.maxY - this.minY) ) this.el.fillStyle = this.color // 填充顏色 this.el.fill() this.el.strokeStyle = '#fff' // 邊框的顏色 this.el.lineCap = 'square' this.el.lineWidth = 3 // 邊框粗細 this.el.stroke() // 畫筆終點 } }
判斷鼠標是否點擊在矩形內部,用來判斷是新建還是拖動矩形。 判斷一個坐標點是否落在矩形的內部很好判斷,就是通過minX、minY、maxX、maxY和傳入的x、y對比就可以了
class Rectangle { ... isInside(x, y) { return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY } }
實現(xiàn)鼠標事件
在鼠標事件中,獲取鼠標的坐標點屬性有好幾種,例如clientX/screenX/offsetX/pageX等,如需分別這幾個屬性的不同,可參考此篇文章,目前我用的是offsetX,因為這個屬性是獲取相對于事件對象的位置,此時事件對象就是canvas。
onmousedown事件 在mousedown事件我們需要做兩件事
判斷鼠標落點是否在已有矩形的內部,如果有就是當前矩形的拖拽事件,如果不是則是新建矩形。 判斷是否落在集合圖形的內部前面我們在幾何圖形類中已經約定好了一個isInside
方法,此時只需遍歷shapes
數組,依次調用每一項的isInside
方法就好。
... // 遍歷數組 getShapes(x, y) { for (let i = this.shapes.length - 1; i >= 0; i--) { const s = this.shapes[i] if (s.isInside(x, y)) { return s } } return null }
如果返回的是null,則進入新建邏輯。新建一個矩形的操作很簡單,就是創(chuàng)建一個Rectangle的實例,new Rectangle(...)
并把new出來的實例添加進shapes數組中。并且此時的onmousemove事件也很簡單,只需把實例中的endX、endY修改成當前move的坐標點。
// 新建 const shape = new Rectangle(that.ctx, '#f00', clickX, clickY) that.shapes.push(shape) that.cvs.onmousemove = function (e) { shape.endX = e.offsetX shape.endY = e.offsetY }
此時如果返回的不是null,說明找到了當前鼠標點擊的圖形,執(zhí)行的是拖拽邏輯。而拖拽邏輯需要計算矩形被拖拽的偏移量,如下圖所示:
此時該矩形的四個坐標都要進行相應的同步修改,并且還要判斷是否移動超出了邊界。
完整代碼如下所示:
class Chart{ ... bindEvent() { const that = this this.cvs.onmousedown = function (e) { this.isClickDown = true const [clickX, clickY] = [e.offsetX, e.offsetY] const shape = that.getShapes(clickX, clickY) if (shape) { // 如果找到圖形,說明是拖拽 shape.action = 'MOVE' const { startX, startY, endX, endY } = shape that.cvs.onmousemove = function (e) { const disX = e.offsetX - clickX const disY = e.offsetY - clickY const newStartX = startX + disX const newEndX = endX + disX const newStartY = startY + disY const newEndY = endY + disY // 判斷是否超出邊界(矩形) if (newStartX < 0 || newEndX > that.cvs.width || newStartY < 0 || newEndY > that.cvs.height) { return } shape.startX = newStartX shape.endX = newEndX shape.startY = newStartY shape.endY = newEndY } } else { // 沒找到,則是新建圖形 const shape = new Rectangle(that.ctx, '#f00', clickX, clickY) that.shapes.push(shape) that.cvs.onmousemove = function (e) { shape.endX = e.offsetX shape.endY = e.offsetY } } } getShapes(x, y) { for (let i = this.shapes.length - 1; i >= 0; i--) { const s = this.shapes[i] if (s.isInside(x, y)) { return s } } return null } }
鼠標按下初始化onmousemove和onmouseup等事件。 onmousemove、onmouseup和onmouseout事件其實很簡單,只需要取消鼠標移動事件和鼠標抬起事件即可
that.cvs.onmouseup = function () { this.isClickDown = false that.cvs.onmousemove = null that.cvs.onmouseup = null } that.cvs.onmouseout = function () { this.isClickDown = false that.cvs.onmousemove = null that.cvs.onmouseup = null }
最終代碼
此時,一個矩形的簡單繪制和拖拽就完成了,后續(xù)的縮放需要的還可以單獨開一篇文章講講。按照這種方式,其他幾何圖形我們可以新建相對應地類就行了,拓展起來就很方便了。
完整代碼
// drawClass.js /** * 生成唯一ID * @param {Number} length 生成id的長度 * @returns */ const genID = (length = 3) => { return Number(Math.random().toString().substr(3, length) + Date.now()).toString(36) } class Chart { constructor(canvasId) { this.cvs = document.getElementById(canvasId) this.ctx = this.cvs.getContext('2d') this.shapes = [] // 保存圖形數據的數組 this.init() // 初始化canvas得寬高 this.bindEvent() // 為canvas綁定鼠標事件 this.draw() // canvas的繪制 this.isClickDown = false // 當前鼠標是否按下 this.isPolygon = false this.currentShape = null // 當前選中的圖形 } init() { const w = 1000, h = 600 this.cvs.width = w this.cvs.height = h } bindEvent() { const that = this this.cvs.onmousedown = function (e) { this.isClickDown = true const [clickX, clickY] = [e.offsetX, e.offsetY] const shape = that.getShapes(clickX, clickY) if (shape) { // 如果找到圖形,說明是拖拽 shape.action = 'MOVE' const { startX, startY, endX, endY } = shape that.cvs.onmousemove = function (e) { const disX = e.offsetX - clickX const disY = e.offsetY - clickY const newStartX = startX + disX const newEndX = endX + disX const newStartY = startY + disY const newEndY = endY + disY // 判斷是否超出邊界(矩形) if (newStartX < 0 || newEndX > that.cvs.width || newStartY < 0 || newEndY > that.cvs.height) { return } shape.startX = newStartX shape.endX = newEndX shape.startY = newStartY shape.endY = newEndY } } else { // 沒找到,則是新建圖形 const shape = new Rectangle(that.ctx, '#f00', clickX, clickY) that.shapes.push(shape) that.cvs.onmousemove = function (e) { shape.endX = e.offsetX shape.endY = e.offsetY const radius = Math.sqrt( Math.pow(e.offsetX - clickX, 2) + Math.pow(e.offsetY - clickY, 2) ) shape.radius = radius } } that.cvs.onmouseup = function () { this.isClickDown = false that.cvs.onmousemove = null that.cvs.onmouseup = null } that.cvs.onmouseout = function () { this.isClickDown = false that.cvs.onmousemove = null that.cvs.onmouseup = null } } } getShapes(x, y) { for (let i = this.shapes.length - 1; i >= 0; i--) { const s = this.shapes[i] if (s.isInside(x, y)) { return s } } return null } draw() { requestAnimationFrame(this.draw.bind(this)) this.ctx.clearRect(0, 0, this.cvs.width, this.cvs.height) // 首先清空canvas // 將shapes里面的圖形重新繪制 for (const s of this.shapes) { s.draw() } } } class Rectangle { constructor(el,color, startX, startY) { this.el = el // 保存需要繪制的載體 this.shapeType = 'RECT' // 當前幾何圖形的類別 this.color = color // 填充的顏色 // 初始化起點,默認起點和終點一樣 this.startX = startX this.startY = startY this.endX = startX this.endY = startY this.action = 'CREATE' // 當前的操作類型 this.id = genID() // 當前實例的唯一id } get minX() { return Math.min(this.startX, this.endX) } get maxX() { return Math.max(this.startX, this.endX) } get minY() { return Math.min(this.startY, this.endY) } get maxY() { return Math.max(this.startY, this.endY) } draw() { this.el.beginPath() // 畫筆起始點 this.el.rect( this.minX, this.minY, (this.maxX - this.minX), (this.maxY - this.minY) ) this.el.fillStyle = this.color // 填充顏色 this.el.fill() this.el.strokeStyle = '#fff' // 邊框的顏色 this.el.lineCap = 'square' this.el.lineWidth = 3 // 邊框粗細 this.el.stroke() //畫筆的終點 } isInside(x, y) { return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY } }
到此這篇關于JS利用原生canvas實現(xiàn)圖形標注功能的文章就介紹到這了,更多相關JS canvas圖形標注內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
基于微信小程序實現(xiàn)人臉數量檢測的開發(fā)步驟
最近項目需求是統(tǒng)計當前攝像頭中的人臉個數,所以下面這篇文章主要給大家介紹了關于基于微信小程序實現(xiàn)人臉數量檢測的相關資料,文中通過圖文介紹的非常詳細,需要的朋友可以參考下2022-12-12javascript實現(xiàn)表格排序 編輯 拖拽 縮放
這篇文章主要介紹了javascript實現(xiàn)表格排序 編輯 拖拽 縮放的方法,效果非常不錯,只是兼容性還有些問題,有待優(yōu)化。2015-01-01原生JavaScript實現(xiàn)的簡單放大鏡效果示例
這篇文章主要介紹了原生JavaScript實現(xiàn)的簡單放大鏡效果,涉及javascript事件響應及頁面元素屬性動態(tài)操作相關實現(xiàn)技巧,需要的朋友可以參考下2018-02-02