方案背景
需求
- 需要對圖片進行標註,導出圖片。
- 需要標註N多圖片最後同時保存。
- 需要根據多邊形區域數據(區域、顏色、名稱)標註。
對應方案
- 用canvas實現塗鴉、圓形、矩形的繪製,最終生成圖片base64編碼用於上傳
- 大量圖片批量上傳很耗時間,爲了提高用戶體驗,改爲只實現圓形、矩形繪製,最終保存成座標,下次顯示時根據座標再繪製。
- 多邊形區域的顯示是根據座標點繪製,名稱顯示的位置爲多邊形質心。
代碼
<template>
<div>
<canvas
:id="radom"
:class="{canDraw: 'canvas'}"
:width="width"
:height="height"
:style="{'width':`${width}px`,'height':`${height}px`}"
@mousedown="canvasDown($event)"
@mouseup="canvasUp($event)"
@mousemove="canvasMove($event)"
@touchstart="canvasDown($event)"
@touchend="canvasUp($event)"
@touchmove="canvasMove($event)">
</canvas>
</div>
</template>
<script>
// import proxy from './proxy.js'
const uuid = require('node-uuid')
export default {
props: {
canDraw: { // 圖片路徑
type: Boolean,
default: true
},
url: { // 圖片路徑
type: String
},
info: { // 位置點信息
type: Array
},
width: { // 繪圖區域寬度
type: String
},
height: { // 繪圖區域高度
type: String
},
lineColor: { // 畫筆顏色
type: String,
default: 'red'
},
lineWidth: { // 畫筆寬度
type: Number,
default: 2
},
lineType: { // 畫筆類型
type: String,
default: 'circle'
}
},
watch: {
info (val) {
if (val) {
this.initDraw()
}
}
},
data () {
return {
// 同一頁面多次渲染時,用於區分元素的id
radom: uuid.v4(),
// canvas對象
context: {},
// 是否處於繪製狀態
canvasMoveUse: false,
// 繪製矩形和橢圓時用來保存起始點信息
beginRec: {
x: '',
y: '',
imageData: ''
},
// 儲存座標信息
drawInfo: [],
// 背景圖片緩存
img: new Image()
}
},
mounted () {
this.initDraw()
},
methods: {
// 初始化繪製信息
initDraw () {
// 初始化畫布
const canvas = document.getElementById(this.radom)
this.context = canvas.getContext('2d')
// 初始化背景圖片
this.img.setAttribute('crossOrigin', 'Anonymous')
this.img.src = this.url
this.img.onerror = () => {
var timeStamp = +new Date()
this.img.src = this.url + '?' + timeStamp
}
this.img.onload = () => {
this.clean()
}
// proxy.getBase64({imgUrl: this.url}).then((res) => {
// if (res.code * 1 === 0) {
// this.img.src = 'data:image/jpeg;base64,'+res.data
// this.img.onload = () => {
// this.clean()
// }
// }
// })
// 初始化畫筆
this.context.lineWidth = this.lineWidth
this.context.strokeStyle = this.lineColor
},
// 鼠標按下
canvasDown (e) {
if (this.canDraw) {
this.canvasMoveUse = true
// client是基於整個頁面的座標,offset是cavas距離pictureDetail頂部以及左邊的距離
const canvasX = e.clientX - e.target.parentNode.offsetLeft
const canvasY = e.clientY - e.target.parentNode.offsetTop
// 記錄起始點和起始狀態
this.beginRec.x = canvasX
this.beginRec.y = canvasY
this.beginRec.imageData = this.context.getImageData(0, 0, this.width, this.height)
// 存儲本次繪製座標信息
this.drawInfo.push({
x: canvasX / this.width,
y: canvasY / this.height,
type: this.lineType
})
}
},
Area (p0,p1,p2) {
let area = 0.0 ;
area = p0.x * p1.y + p1.x * p2.y + p2.x * p0.y - p1.x * p0.y - p2.x * p1.y - p0.x * p2.y;
return area / 2 ;
},
// 計算多邊形質心
getPolygonAreaCenter (points) {
let sum_x = 0;
let sum_y = 0;
let sum_area = 0;
let p1 = points[1];
for (var i = 2; i < points.length; i++) {
let p2 = points[i];
let area = this.Area(points[0],p1,p2) ;
sum_area += area ;
sum_x += (points[0].x + p1.x + p2.x) * area;
sum_y += (points[0].y + p1.y + p2.y) * area;
p1 = p2 ;
}
return {
x: sum_x / sum_area / 3,
y: sum_y / sum_area / 3
}
},
// 根據座標信息繪製圖形
drawWithInfo () {
this.info.forEach(item => {
this.context.beginPath()
if (!item.type) {
// 設置顏色
this.context.strokeStyle = item.regionColor
this.context.fillStyle = item.regionColor
// 繪製多邊形的邊
if (typeof item.region === 'string') {
item.region = JSON.parse(item.region)
}
item.region.forEach(point => {
this.context.lineTo(point.x * this.width, point.y * this.height)
})
this.context.closePath()
// 在多邊形質心標註文字
let point = this.getPolygonAreaCenter(item.region)
this.context.fillText(item.areaName, point.x * this.width, point.y * this.height)
} else if (item.type === 'rec') {
this.context.rect(item.x * this.width, item.y * this.height, item.w * this.width, item.h * this.height)
} else if (item.type === 'circle') {
this.drawEllipse(this.context, (item.x + item.a) * this.width, (item.y + item.b) * this.height, item.a > 0 ? item.a * this.width : -item.a * this.width, item.b > 0 ? item.b * this.height : -item.b * this.height)
}
this.context.stroke()
})
},
// 鼠標移動時繪製
canvasMove (e) {
if (this.canvasMoveUse && this.canDraw) {
// client是基於整個頁面的座標,offset是cavas距離pictureDetail頂部以及左邊的距離
let canvasX = e.clientX - e.target.parentNode.offsetLeft
let canvasY = e.clientY - e.target.parentNode.offsetTop
if (this.lineType === 'rec') { // 繪製矩形時恢復起始點狀態再重新繪製
this.context.putImageData(this.beginRec.imageData, 0, 0)
this.context.beginPath()
this.context.rect(this.beginRec.x, this.beginRec.y, canvasX - this.beginRec.x, canvasY - this.beginRec.y)
let info = this.drawInfo[this.drawInfo.length - 1]
info.w = canvasX / this.width - info.x
info.h = canvasY / this.height - info.y
} else if (this.lineType === 'circle') { // 繪製橢圓時恢復起始點狀態再重新繪製
this.context.putImageData(this.beginRec.imageData, 0, 0)
this.context.beginPath()
let a = (canvasX - this.beginRec.x) / 2
let b = (canvasY - this.beginRec.y) / 2
this.drawEllipse(this.context, this.beginRec.x + a, this.beginRec.y + b, a > 0 ? a : -a, b > 0 ? b : -b)
let info = this.drawInfo[this.drawInfo.length - 1]
info.a = a / this.width
info.b = b / this.height
}
this.context.stroke()
}
},
// 繪製橢圓
drawEllipse (context, x, y, a, b) {
context.save()
var r = (a > b) ? a : b
var ratioX = a / r
var ratioY = b / r
context.scale(ratioX, ratioY)
context.beginPath()
context.arc(x / ratioX, y / ratioY, r, 0, 2 * Math.PI, false)
context.closePath()
context.restore()
},
// 鼠標擡起
canvasUp (e) {
if (this.canDraw) {
this.canvasMoveUse = false
}
},
// 獲取座標信息
getInfo () {
return this.drawInfo
},
// 清空畫布
clean () {
this.context.drawImage(this.img, 0, 0, this.width, this.height)
this.drawInfo = []
if (this.info && this.info.length !== 0) this.drawWithInfo()
}
}
}
</script>
<style lang="scss" scoped>
.canvas{
cursor: crosshair;
}
</style>
必須傳入的參數
- 圖片路徑
url: string
- 繪圖區域寬度
width: string
- 繪圖區域高度
height: string
選擇傳入的參數
- 是否可以繪製,默認true
canDraw: boolean
- 座標點信息,不傳入則不繪製
info: string
- 是否可繪製,默認true
canDraw: boolean
- 繪圖顏色,默認red
lineColor: string
- 繪圖筆寬度,默認2
lineWidth: number
- 繪圖筆類型,rec、circle,默認rec
lineType: string
可以調用的方法
- 清空畫布
clean()
- 返回座標點信息
getInfo()
特殊說明
- canvas對象不能獲得座標,是通過父元素座標獲取的,所以該組件的父元素以上的層級不能有太多的定位、嵌套,否則繪製座標會偏移。
- 域名不同的圖片可能存在跨域問題,看過很多資料沒有太好的辦法,最後項目中是用node服務做了一個圖片轉爲base64的接口,再給canvas繪製解決的。並不一定適用於其他項目,如果有更好的辦法解決歡迎分享。
- 導出座標點數據只能導出規則圖案的座標點,因爲隨意塗鴉的座標點太多時會崩潰的(雖然沒試過具體到什麼程度會崩潰),如果有高性能的實現方式歡迎分享。
- 如果塗鴉後保存再請求圖片url出現請求不到的情況,是因爲CDN緩存的問題,在圖片路徑後面拼個隨機碼就可以解決。