欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

JS利用原生canvas實(shí)現(xiàn)圖形標(biāo)注功能

 更新時(shí)間:2024年03月07日 14:24:23   作者:一枕槐安  
這篇文章主要為大家詳細(xì)介紹了JS如何利用原生canvas實(shí)現(xiàn)圖形標(biāo)注功能,支持矩形、多邊形、線段、圓形等已繪制的圖形進(jìn)行縮放,移動(dòng),需要的可以參考下

由于工作需要,項(xiàng)目中要求實(shí)現(xiàn)一個(gè)功能—在視頻或者圖片上進(jìn)行圖形標(biāo)注,支持矩形、多邊形、線段、圓形、折線,已繪制的圖形可以進(jìn)行縮放,移動(dòng)。

完整功能源代碼在這個(gè)倉(cāng)庫(kù),感興趣的可以clone下來(lái)跑一下。

接下來(lái)我將實(shí)現(xiàn)一個(gè)dmeo來(lái)展示其中最簡(jiǎn)單的圖形—矩形的創(chuàng)建。

初始化頁(yè)面

<!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>

該頁(yè)面只有一個(gè)canvas,而功能的實(shí)現(xiàn)在drawClass里面,這里我實(shí)現(xiàn)了一個(gè)Chart類,需要傳入一個(gè)canvas的id,表示后續(xù)的繪制功能都是在這個(gè)canvas上實(shí)現(xiàn)的。

創(chuàng)建基礎(chǔ)類Chart

首先梳理下要實(shí)現(xiàn)一個(gè)圖形標(biāo)注需要涉及到什么事件

用戶在canvas上按下鼠標(biāo),說(shuō)明用戶想要開(kāi)始畫(huà)一個(gè)矩形進(jìn)行標(biāo)注了,所以canvas上需要綁定一個(gè)onmousedown 事件。相應(yīng)的繪制過(guò)程也涉及到了onmousemove 事件,而用戶松開(kāi)鼠標(biāo)表示繪制已完成,即需要onmouseup事件。同時(shí)為了兼容這樣一種情況—用戶在繪制過(guò)程中鼠標(biāo)移出了canvas的范圍(此時(shí)的鼠標(biāo)事件都是綁定在canvas上的),這種情況無(wú)法觸發(fā)canvas的onmouseup事件,所以為canvas加上onmouseout,鼠標(biāo)移出默認(rèn)代表繪制完成。

初始化代碼如下

class Chart {
  constructor(canvasId) {
    this.cvs = document.getElementById(canvasId)
    this.ctx = this.cvs.getContext('2d')
    this.shapes = [] // 保存圖形數(shù)據(jù)的數(shù)組
    this.init() // 初始化canvas得寬高
    this.bindEvent() // 為canvas綁定鼠標(biāo)事件
    this.draw() // canvas的繪制
    this.isClickDown = false // 當(dāng)前鼠標(biāo)是否按下
    this.currentShape = null // 當(dāng)前選中的圖形
  }
  init() {
    const w = 1000,
      h = 600
    this.cvs.width = w
    this.cvs.height = h
  }
  bindEvent() {
    this.cvs.onmousedown = (e) => {
      this.isClickDown = true
			// 鼠標(biāo)按下的時(shí)候創(chuàng)建其他鼠標(biāo)事件
      that.cvs.onmousemove = function (e){}
      that.cvs.onmouseup = function (e){}
      that.cvs.onmouseout = function (e){}
    }
  }
  draw() {}
}

為了后續(xù)拓展,我將各種圖形封裝成一個(gè)個(gè)類,如矩形封裝成class **Rectangle 。這樣做的好處是我們只需要規(guī)定這樣的圖形類內(nèi)部都有某些屬性和方法給Chart中的draw調(diào)用就行,比如我們可以把繪制矩形的實(shí)現(xiàn)寫(xiě)在Rectangle類的draw函數(shù)上,而在Chart中的draw上只需調(diào)用new Rectangle().draw() 即可。要注意的是,draw方法是隨著mousemove而要不斷的刷新canvas重新繪制的,此時(shí)最好使用requestAnimationFrame方法讓瀏覽在合適的時(shí)機(jī)調(diào)用draw。此時(shí)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()
    }
  }
}

實(shí)現(xiàn)Rectangle類

首先看一下矩形需要的幾點(diǎn)要素

  • 填充的顏色
  • 矩形的初始位置(即鼠標(biāo)按下的位置)
  • 矩形的終點(diǎn)位置(即鼠標(biāo)抬起的位置,也是鼠標(biāo)移動(dòng)的最后位置)
  • 矩形需要繪制在哪里(此時(shí)需要繪制在canvas上)
  • 矩形的初始位置可能會(huì)大于終點(diǎn)位置(鼠標(biāo)按下后往左上角繪制),此時(shí)可用class語(yǔ)法中的get方法獲取minX、maxX、minY、maxY
  • 因?yàn)橥悗缀螆D形的實(shí)例可能會(huì)有很多,所以每個(gè)實(shí)例需要一個(gè)id

初始代碼如下:

/**
 * 生成唯一ID
 * @param {Number} length  生成id的長(zhǎng)度
 * @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' // 當(dāng)前幾何圖形的類別
    this.color = color // 填充的顏色
    // 初始化起點(diǎn),默認(rèn)起點(diǎn)和終點(diǎn)一樣
    this.startX = startX
    this.startY = startY
    this.endX = startX
    this.endY = startY
    this.action = 'CREATE'  // 當(dāng)前的操作類型
    this.id = genID() // 當(dāng)前實(shí)例的唯一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() {}
}

實(shí)現(xiàn)Rectangle類的draw函數(shù) 幸運(yùn)的是canvas得API中已經(jīng)有了繪制矩形的方法,不用我們一條線一條線的自己畫(huà)了。該方法是 canvas.rect(startX,startY,width,height) 四個(gè)參數(shù)分別是矩形的起點(diǎn)橫坐標(biāo)和縱坐標(biāo),矩形的寬高,這也是為什么需要minX和minY。代碼如下:

class Rectangle {
	...
	 draw() {
    this.el.beginPath()  // 畫(huà)筆起始點(diǎn)
    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 // 邊框粗細(xì)
    this.el.stroke() // 畫(huà)筆終點(diǎn)
  }
}

判斷鼠標(biāo)是否點(diǎn)擊在矩形內(nèi)部,用來(lái)判斷是新建還是拖動(dòng)矩形。 判斷一個(gè)坐標(biāo)點(diǎn)是否落在矩形的內(nèi)部很好判斷,就是通過(guò)minX、minY、maxX、maxY和傳入的x、y對(duì)比就可以了

class Rectangle {
	...

  isInside(x, y) {
    return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
  }
}

實(shí)現(xiàn)鼠標(biāo)事件

在鼠標(biāo)事件中,獲取鼠標(biāo)的坐標(biāo)點(diǎn)屬性有好幾種,例如clientX/screenX/offsetX/pageX等,如需分別這幾個(gè)屬性的不同,可參考此篇文章,目前我用的是offsetX,因?yàn)檫@個(gè)屬性是獲取相對(duì)于事件對(duì)象的位置,此時(shí)事件對(duì)象就是canvas。

onmousedown事件 在mousedown事件我們需要做兩件事

判斷鼠標(biāo)落點(diǎn)是否在已有矩形的內(nèi)部,如果有就是當(dāng)前矩形的拖拽事件,如果不是則是新建矩形。 判斷是否落在集合圖形的內(nèi)部前面我們?cè)趲缀螆D形類中已經(jīng)約定好了一個(gè)isInside方法,此時(shí)只需遍歷shapes數(shù)組,依次調(diào)用每一項(xiàng)的isInside 方法就好。

...
// 遍歷數(shù)組
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,則進(jìn)入新建邏輯。新建一個(gè)矩形的操作很簡(jiǎn)單,就是創(chuàng)建一個(gè)Rectangle的實(shí)例,new Rectangle(...) 并把new出來(lái)的實(shí)例添加進(jìn)shapes數(shù)組中。并且此時(shí)的onmousemove事件也很簡(jiǎn)單,只需把實(shí)例中的endX、endY修改成當(dāng)前move的坐標(biāo)點(diǎn)。

// 新建
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
}

此時(shí)如果返回的不是null,說(shuō)明找到了當(dāng)前鼠標(biāo)點(diǎn)擊的圖形,執(zhí)行的是拖拽邏輯。而拖拽邏輯需要計(jì)算矩形被拖拽的偏移量,如下圖所示:

此時(shí)該矩形的四個(gè)坐標(biāo)都要進(jìn)行相應(yīng)的同步修改,并且還要判斷是否移動(dòng)超出了邊界。

完整代碼如下所示:

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) {
        // 如果找到圖形,說(shuō)明是拖拽
        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 {
        // 沒(méi)找到,則是新建圖形
        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
  }
}

鼠標(biāo)按下初始化onmousemove和onmouseup等事件。 onmousemove、onmouseup和onmouseout事件其實(shí)很簡(jiǎn)單,只需要取消鼠標(biāo)移動(dòng)事件和鼠標(biāo)抬起事件即可

    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
    }

最終代碼

此時(shí),一個(gè)矩形的簡(jiǎn)單繪制和拖拽就完成了,后續(xù)的縮放需要的還可以單獨(dú)開(kāi)一篇文章講講。按照這種方式,其他幾何圖形我們可以新建相對(duì)應(yīng)地類就行了,拓展起來(lái)就很方便了。

完整代碼

// drawClass.js
/**
 * 生成唯一ID
 * @param {Number} length  生成id的長(zhǎng)度
 * @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 = [] // 保存圖形數(shù)據(jù)的數(shù)組
    this.init() // 初始化canvas得寬高
    this.bindEvent() // 為canvas綁定鼠標(biāo)事件
    this.draw() // canvas的繪制
    this.isClickDown = false // 當(dāng)前鼠標(biāo)是否按下
    this.isPolygon = false
    this.currentShape = null // 當(dāng)前選中的圖形
  }

  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) {
        // 如果找到圖形,說(shuō)明是拖拽
        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 {
        // 沒(méi)找到,則是新建圖形
        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' // 當(dāng)前幾何圖形的類別
    this.color = color // 填充的顏色
    // 初始化起點(diǎn),默認(rèn)起點(diǎn)和終點(diǎn)一樣
    this.startX = startX
    this.startY = startY
    this.endX = startX
    this.endY = startY
    this.action = 'CREATE'  // 當(dāng)前的操作類型
    this.id = genID() // 當(dāng)前實(shí)例的唯一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()  // 畫(huà)筆起始點(diǎn)
    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 // 邊框粗細(xì)
    this.el.stroke() //畫(huà)筆的終點(diǎn)
  }

  isInside(x, y) {
    return x >= this.minX && x <= this.maxX && y >= this.minY && y <= this.maxY
  }
}

到此這篇關(guān)于JS利用原生canvas實(shí)現(xiàn)圖形標(biāo)注功能的文章就介紹到這了,更多相關(guān)JS canvas圖形標(biāo)注內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

最新評(píng)論