vue组件:canvas实现图片涂鸦功能
方案背景
需求
- 需要对图片进行标注,导出图片。
- 需要标注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缓存的问题,在图片路径后面拼个随机码就可以解决。
原文地址:https://segmentfault.com/a/1190000016852958
vue组件:canvas实现图片涂鸦功能的更多相关文章
- vue项目js实现图片放大镜功能
效果图: 我写的是vue的组件形式,方便复用,图片的宽高,缩放的比例可以自己定义 magnifier.vue <template> <div class="magnif ...
- vue组件利用formdata图片预览以及上传《转载》
转载修改 在项目中直接新建一个单文件页,复制一下代码即可 upload组件: <template> <div class="vue-uploader" ...
- vue组件利用formdata图片预览以及上传
转载修改 在项目中直接新建一个单文件页,复制一下代码即可 upload组件: <template> <div class="vue-uploader" ...
- vue 实现点击图片放大
作者QQ:1095737364 QQ群:123300273 欢迎加入! 1.建立子组件,来实现图片方法功能: BigImg.vue <template> <!-- 过渡 ...
- vue组件如何被其他项目引用
自己写的vue组件怎么才能让其他人引用呢,或者是共用组件如何让其他项目引用.本文就粗细的介绍下,如有疑问欢迎共同讨论.在这里你能了解下如下知识点: 1. 如何发布一个包到npmjs仓库上 2.如何引用 ...
- Laravel 项目中编写第一个 Vue 组件
和 CSS 框架一样,Laravel 不强制你使用什么 JavaScript 客户端框架,但是开箱对 Vue.js 提供了良好的支持,如果你更熟悉 React 的话,也可以将默认的脚手架代码替换成 R ...
- vue中图片放大镜功能
仿淘宝详情页图片鼠标移过去可对图片放大显示在右侧 效果图如下图,此功能支持PC端与移动端 接下来进入代码实现环节: 先准备两张图片,一张小图片叫 '土味.jpg',大小160*91:一张大图片叫 ' ...
- 通过base64实现图片下载功能(基于vue)
1. 使用场景 当我们处理图片下载功能的时候,如果本地的图片,那么是可以通过canvas获得图片的base64的,方法如下.但是如果图片的url存在跨域问题的话,下面的方法将行不通,这时候我们可以另辟 ...
- 从web编辑器 UEditor 中单独提取图片上传,包含多图片单图片上传以及在线涂鸦功能
UEditor是由百度web前端研发部开发所见即所得富文本web编辑器,具有轻量,可定制,注重用户体验等特点,开源基于MIT协议,允许自由使用和修改代码.(抄的...) UEditor是非常好用的富文 ...
随机推荐
- linux 03 命令 续
linux 03 命令 续 一.vim 两种操作方式:新文件 pyvip@Vip:~/demo/2_3$ vim demo.txt #操作一个新文件 一开始进入的是命令模式,按i进入插入模式,开始编辑 ...
- 前端JavaScript(3)-关于DOM操作的相关案例,JS中的面向对象、定时器、BOM、位置信息
小例子: 京东购物车 京东购物车效果: 实现原理: 用2个盒子,就可以完整效果. 先让上面的小盒子向下移动1px,此时就出现了压盖效果.小盒子设置z-index压盖大盒子,将小盒子的下边框去掉,就可以 ...
- 线程池(3)Executors.newCachedThreadPool
例子: ExecutorService es = Executors.newCachedThreadPool(); try { for (int i = 0; i < 20; i++) { Ru ...
- 自定义view(14)使用Path绘制复杂图形
灵活使用 Path ,可以画出复杂图形,就像美术生在画板上画复杂图形一样.程序员也可以用代码实现. 1.样板图片 这个是个温度计,它是静态的,温度值是动态变化的,所以要自定义个view.动态显示值,温 ...
- NET Core 2.0 使用支付宝
ASP.NET Core 2.0 使用支付宝PC网站支付 前言 最近在使用ASP.NET Core来进行开发,刚好有个接入支付宝支付的需求,百度了一下没找到相关的资料,看了官方的SDK以及Demo ...
- Spark Mllib里如何建立密集向量和稀疏向量(图文详解)
不多说,直接上干货! 具体,见 Spark Mllib机器学习实战的第4章 Mllib基本数据类型和Mllib数理统计
- 关于使用mybatis的分页插件问题
首先我需要导入架包 1.pagehelper 如果你是在mybatis中配置分页‘ 如下代码 <plugins> <plugin interceptor="com.gith ...
- ABAP事件的简单用法
1.1.事件: 用于捕获某类对象状态的改变来触发事件的方法,并进行处理 1.2.定义:可以在类或接口中进行声明 EVENTS|CLASS-EVENTS evt EXPORTING … VALUE(p ...
- 【Unity3D】射箭打靶游戏(简单工厂+物理引擎编程)
打靶游戏: 1.靶对象为 5 环,按环计分: 2.箭对象,射中后要插在靶上: 3.游戏仅一轮,无限 trials: 增强要求: 添加一个风向和强度标志,提高难度 游戏成品图: U ...
- 【extjs6学习笔记】0.1 准备:基础概念(02)
Ext 类 Ext 是一个全局单例的对象,在 Sencha library 中它封装了所有的类和许多实用的方法.许多常用的函数都定义在 Ext 对象里.它还提供了像其他类中一些频繁使用的方法的快速调用 ...