注意:最近蛮多童鞋想要了解关于截图拖动及导出的全部源码,我这里原生写了个demo,参考仓库地址如下!
好了废话不多说,开始正文,这次主要做的是关于canvas图像绘制的部分,要实现的功能主要是绘制、对canvas的放大,缩小,拖动以及截图
那我们先来大概了解下canvas:
1、canvas略解
<canvas ref="canvas" id="canvas" :width="width" :height="height" />
this.canvas = document.getElementById('canvas') // 画布对象
this.context = this.canvas.getContext('2d') // 画布显示二维图片
// 注意设置百分比的话,父盒子要设置实际宽高
width: 200px 或者 100%
height: 200px 或者 100%
宽高是必须加上的,当然你也可以选择动态宽高,可以设置宽高百分比喔,但是我建议还是尽量设置实际像素~(因为当你涂鸦canvas时,不设置实际像素距离的话,canvas就会出现偏离,线宽也会是百分比,所以强烈建议实际像素)
canvas用于在页面上绘制图像(可以是自己的图片,也可以绘制自定义矢量图形:矩形等),其实我们不用担心大量的重绘canvas,现在浏览器完全扛得住这压力,非常流畅丝滑
下面我来大概讲解下绘制图像,绘制自定义矢量图形其他博客上有很多,我这里就先省略了
2、canvas绘制图像
截图:
首先整个灰色部分都是canvas(上面操作栏不是canvas喔),我们可以看到我们所使用的图是位于左上角的,canvas绘制是以左上角(0,0)为原始点的
2.1、图像绘制(drawImage用法)
可以说drawImage方法是绘制图像的独有方法,只有调用了这个方法才能进行绘制,那在绘制之前应该注意些什么呢?
1.context.clearRect()
首先要将canvas里进行清除(例如你之前已经绘制过了,想要重新绘制,则应该先清除)
this.context.clearRect(0, 0, width, height);
这里的第一二参数分别为你要清除的区域起始点的x,y轴坐标,第三四参数分别为你要清除的区域的宽高,我们就只需要写入 0, 0, width, height 来清除整个canvas区域
2.image.onload
在绘制前我们得对要绘制的图片有了解,比如图片宽高,是否跨域(这是个重点,后面我讲一下,很常遇到),并且drawImage必须要在image.onload里面调用才行,因为onload是异步,你将drawImage放在外面会有偶发性的bug(图像一会能绘制,一会不能绘制),这是全局重点
我们在第一次mounted时,只需要调用下面这一个loadImg方法,首先
this.img:代表我们当前图像的一个变量,方便后续操作
/**
* 加载读取图片属性
*/
loadImg() {
this.img = new Image()
this.updateImageUrl()
return this.initImage()
},
updateImageUrl: 用来更换this.img图像的url的
/**
* 这里用来更换图片url的
*/
updateImageUrl() {
this.img.src = xxx
},
这里是当前比较重要的一个步骤,为什么要用promise呢,因为我们知道所有操作都要在图片onload异步回调里,那么使用promise能够较好的配合异步使用
注意:onload只在 Image类被实例化后赋值src才会触发,只在赋值src才会触发,才会触发!!
这里type只是为了方便不重复计算当前图片宽高等参数,当你传入的图片宽高规模都一致时,那么只需要在第一次进来时 this.initImage(),后续更改图片路径后只需要 this.initImage(false)就可以了
/**
* 绘制钩子初始化
* @param {*} type 是否重新计算pageImage各参数
*/
initImage(type = true) {
const img = new Image()
img.src = this.img.src
return new Promise((resolve, reject) => {
const _this = this
img.onload = function() {
if (type) {
_this.calcImage(img)
}
resolve()
}
})
},
calcImage: 计算当前canvas绘制参数
/**
* 根据图片来计算当前canvas绘制参数
* arg 代表当前图片img
*/
calcImage(arg) {
if (arg.width > this.width) {
this.pageImage.imgScale = this.width / arg.width;
if (arg.height * this.pageImage.imgScale > this.height) {
this.pageImage.imgScale = this.height / arg.height;
}
} else if (arg.height > this.height) {
this.pageImage.imgScale = this.height / this.height;
}
// 然后根据计算出的imgScale 为当前100%展示的基准。
this.pageImage.unit = 100 / Number(this.pageImage.imgScale.toFixed(4)) // 转换比例生成 保存4位小数 更精确
this.pageImage.maxImgScale = Number((this.pageImage.imgScale * 2).toFixed(4)) // 放大最大2倍数生成
this.pageImage.minImgScale = Number((this.pageImage.imgScale * 0.2).toFixed(4)) // 缩小最小0.2倍数生成
// 下面参数是canvas拖动前后需要
this.beforePos = this.afterPos = {
x: this.pageImage.imgX,
y: this.pageImage.imgY
}
},
canvas绘制参数
pageImage: {
imgX: 0, // canvas图像距离左上角 x轴距离
imgY: 0, // canvas图像距离左上角 y轴距离
imgScale: 1, // canvas实际默认为1
minImgScale: 0.2, // canvas实际默认为1最小为0.2
maxImgScale: 2, // canvas实际默认为1最大为2
unit: 100, // 实际和展示canvas 中间的转换比例单位
scale: 100 // 展示canvas比例
}
3.图像绘制 drawImage(核心)
上面已经将图像的各部分参数计算出来了,那现在开始绘制图像了
this.loadImg().then(() => {
this.drawImage()
})
下面我对drawImage参数的理解
下面s开头的,都是对图片本身的裁剪参数,d开头的,都是对canvas放置地方的参数
this.context.drawImage(image, dx, dy);
this.context.drawImage(image, dx, dy, dwidth, dheight);
this.context.drawImage(image, sx, sy, swidth, sheight, dx, dy, dwidth, dheight);
当然 drawImage 第一参数也可以是canvas画布噢,常见的使用就是图片或者画布
我们一般都想对图片进行完全展示,那么sx, sy 就设置为 0,0 ,swidth,sheight就将之前我们计算出的图片实际宽高给设置进来。想展示在canvas左上角,那么dx,dy就为0,0,最后两个参数比较重要,是关于缩放比例的(计算缩放比例在上面calcImage函数内)
我们想要全部展示,那么最后两个参数就设置为:
this.img.width * this.pageImage.imgScale, this.img.height * this.pageImage.imgScale
实例结果如下
/**
* 绘制图像
*/
drawImage() {
this.clearImage() // 绘制前先清除
this.context.drawImage(
this.img, // 规定要使用的图像、画布或视频。
0, 0, // 开始剪切的 x 坐标位置。
this.img.width, this.img.height, // 被剪切图像的高度。
this.pageImage.imgX, this.pageImage.imgY, // 在画布上放置图像的 x 、y坐标位置。
this.img.width * this.pageImage.imgScale, this.img.height * this.pageImage.imgScale // 要使用的图像的宽度、高度
)
}
2.2、图像缩放
随时监听imgScale参数的大小,以此来实时更新绘制图像
watch: {
/**
*监听imgScale变化 更新页面上的显示比例 unit = imgScale * unit
*/
'pageImage.imgScale': {
handler(val) {
if (val && !this.lockInit) {
if (val > this.pageImage.maxImgScale) { // 当放大到最大倍数时 不能再放大
val = this.pageImage.maxImgScale
} else if (val < this.pageImage.minImgScale) {
val = this.pageImage.minImgScale
} else {
this.realPosChange()
}
this.pageImage.scale = Number((val * this.pageImage.unit).toFixed())
}
},
deep: true
}
}
随着缩放参数改变 ,来计算canvas的左上角位置参数,并随后进行绘制
/**
* 随着scale改变 新canvas的位置参数
*/
realPosChange() {
this.pageImage.imgX = (1 - this.pageImage.imgScale) * this.afterPos.x + (this.beforePos.x - this.afterPos.x)
this.pageImage.imgY = (1 - this.pageImage.imgScale) * this.afterPos.y + (this.beforePos.y - this.afterPos.y)
this.drawImage() // 重新绘制图片
},
1、按钮点击放大缩小
/**
* canvas图像大小操作 放大 1 缩小 -1 重置 0
*/
scaleControl(type) {
switch (type) {
case -1:
this.pageImage.imgScale -= 100 / this.pageImage.unit / 10
if (this.pageImage.imgScale < this.pageImage.minImgScale) { // 当放大到最大倍数时 不能再放大
this.pageImage.imgScale = this.pageImage.minImgScale
}
break
case 0:
this.pageImage.imgScale = 100 / this.pageImage.unit
break
case 1:
this.pageImage.imgScale += 100 / this.pageImage.unit / 10
if (this.pageImage.imgScale > this.pageImage.maxImgScale) { // 当放大到最大倍数时 不能再放大
this.pageImage.imgScale = this.pageImage.maxImgScale
}
break
}
},
2、鼠标滑轮放大缩小
添加鼠标监听,切记要在beforeDestroy时移除监听喔~
this.$refs.canvas.addEventListener('mousewheel', this.mouseWheelEvent)
this.$refs.canvas.removeEventListener('mousewheel', this.mouseWheelEvent)
/**
* 鼠标滑轮监听
* @param {*} event
*/
mouseWheelEvent(event) {
this.beforePos = this.pointsToCanvas(event.clientX, event.clientY);
this.afterPos =
{
x: Number(((this.beforePos.x - this.pageImage.imgX) / this.pageImage.imgScale).toFixed(2)),
y: Number(((this.beforePos.y - this.pageImage.imgY) / this.pageImage.imgScale).toFixed(2))
}
if (event.wheelDelta > 0) { // 每次放大10%
this.pageImage.imgScale += 100 / this.pageImage.unit / 10;
if (this.pageImage.imgScale >= this.pageImage.maxImgScale) { // 当放大到最大倍数时 不能再放大
this.pageImage.imgScale = this.pageImage.maxImgScale
}
} else { // 每次缩小10%
this.pageImage.imgScale -= 100 / this.pageImage.unit / 10;
if (this.pageImage.imgScale <= this.pageImage.minImgScale) { // 当缩小到最小倍数时 不能再缩小
this.pageImage.imgScale = this.pageImage.minImgScale
}
}
}
/**
* 坐标互转
* @param {*} x
* @param {*} y
*/
pointsToCanvas(x, y) {
const box = this.canvas.getBoundingClientRect()
return {
x: x - box.left - (box.width - this.myCanvas.width) / 2,
y: y - box.top - (box.height - this.myCanvas.height) / 2
};
}
2.3、图像拖拽
添加dom鼠标事件监听,切记要在beforeDestroy时移除监听喔~
draggle 是当前拖动状态,默认一进来是false,只有在拖拽时,才是true,这里相当于是一直在重新绘制canvas,但是不用担心性能,canvas本来就专门做这些的,一点都不会卡
this.canvas.onmousedown = e => {
this.draggle = true;
this.beforePos = this.pointsToCanvas(e.clientX, e.clientY)
}
this.canvas.onmousemove = e => {
if (this.draggle) {
document.onmousemove = (e) => {
e.preventDefault();
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
}
this.afterPos = this.pointsToCanvas(e.clientX, e.clientY)
const x = this.afterPos.x - this.beforePos.x
const y = this.afterPos.y - this.beforePos.y
this.pageImage.imgX += x
this.pageImage.imgY += y
this.beforePos = JSON.parse(JSON.stringify(this.afterPos))
this.drawImage()
}
}
this.canvas.onmouseup = e => {
this.draggle = false
}
this.canvas.onmousedown = null
this.canvas.onmousemove = null
this.canvas.onmouseup = null
2.4、图像截图
其实这个单独做起来也不难,一个canvas搞定很多其他博客有。但是要是和我上面代码结合的话,就比较麻烦了,所以你要用我上面的放大缩小拖拽,然后再想弄截图,就下面评论或私聊我吧,毕竟截图更麻烦,博客讲不清楚。。。
3、疑难问题解答
1、图片跨域如何解决?
这种一般都是在图片服务器配置跨域参数解决
header("Access-Control-Allow-Origin: *"); // 任意域名
header("Access-Control-Allow-Origin: xxx"); // 指定域名
如果还是不行,则按照下面方式解决
const image = new Image()
image.crossOrigin = 'Anonymous'
image.src = xxx
2、canvas.toBlob转image
this.canvas.toBlob((blob) => { resolve(this.blob2file(blob)) }, 'image/png', 1)
/**
* 随机id
*/
uuid() {
let d = new Date().getTime();
const uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16);
});
return uuid
},
/**
* canvas转base64
* @param {*} blob
* @param {*} type
* @param {*} name
*/
blob2file(blob, type = 'png', name = '') {
const fileName = name || this.uuid() + '.' + type
const file = new File([blob], fileName, { type: blob.type, lastModified: Date.now() })
return file
}
3、getImageData想转image?
我不建议这么转,虽然getImageData获取到所有像素点,并且可以修改,但是貌似不咋好写后续(实际就是我没弄出来,你们可以试试),我还是建议toBlob
4、toDataUrl好还是toBlob好?
我建议使用toBlob,这个网上有很多说法了,我就不一一解释了
5、获取图像数据并绘制使用getImageData配合putImageData?
如果你只是想要将涂鸦后的canvas转图片并且只展示的话,就可以getImageData配合putImageData,如果你想通过接口传给后端,那我建议你是用toBlob
注意getImageData配合putImageData使用需要新建一个匿名canvas噢!
举例:我有一个canvas可以随时进行涂鸦,但是有俩按钮可以放大缩小,此时如何保持之前的canvas涂鸦记录不消失呢?
知识点:canvas在宽高改变时,自身内容是一定会被销毁的
所以在点击放大缩小前,先将canvas数据获取到,赋值给一个变量,然后创建匿名canvas,将canvasData 通过putImageData绘制上去,然后将匿名canvas返回回来,然后将当前canvas数据清除掉,再通过drawImage绘制canvas,就可以了噢
// 匿名canvas保存已经存在的canvas图形对象
createCanvas(canvasData) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
canvas.width = this.canvas.width
canvas.height = this.canvas.height
ctx.putImageData(canvasData, 0, 0)
return canvas
},
// 放大缩小canvas的保存绘制
resize(width, height){
const canvasData = this.ctx.getImageData(0, 0, this.canvas.width, this.canvas.height); // 提取画布数据
const annoyCanvas = this.createCanvas(canvasData)
this.canvas.width = width
this.canvas.height = height
this.$nextTick(() => {
// 清除画布
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height)
// 改变完宽高后,重绘画布
this.ctx.drawImage(annoyCanvas, 0, 0, annoyCanvas.width, annoyCanvas.height, 0, 0, width, height);
})
}
6、canvas图片污染?
Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.
一般为上述描述,出现这种情况,你就得新开辟一个canvas,然后进行绘制,不在之前已经绘制过的canvas再次操作
document.createElement('canvas')
7、在绘制或编辑矩形框时出现黑色拒绝符号/卡顿?
加上下面这几行阻止浏览器默认事件即可以解决卡顿导致的事件丢失问题,提升效率
document.onmousemove = (e) => {
e.preventDefault();
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
}
8、为什么图片没有跨域,但是图片的onload事件就是不触发?
有细心的童鞋会发现,对同一张图片进行多次赋相同的值,除了第一次onload生效外,后续的onload事件不会触发,这个时候与跨域无关,因为跨域会报错。我觉得可能是与浏览器的缓存机制有关,所以解决方式是后面加上时间戳,每次都去请求新的图片地址。
const image = new Image()
image.crossOrigin = 'Anonymous'
image.src = `${this.xxx}?t=${Date.now()}`
image.onload = () => {}
9、不想通过canvas实现拖动,想通过dom事件实现拖动?
v-drag 为自定义拖拽指令,用于一般dom在父节点范围内拖动,想 了解的在我另一篇博客查看
<div
v-drag
>
<img src="xxx" />
</div>
10、 文章开头的矩形框截图 是可以直接使用的吗?
当然,全原生js实现,可以直接用,但是当你的底图放大缩小是通过transform: scale 来实现的,那就不可以噢~,这是为啥呢? 因为scale放大缩小后,他原始的宽高并没有改变,所以当放大缩小后,我们截图dom框所需要的相关距离就不是当前页面的真实距离,所以一定切记噢! 图片底图通过scale来操作的就不能使用我的矩形框方法噢!
11、drawImage出现 Failed to execute 'drawImage' on 'CanvasRenderingContext2D'报错
报错原因是 drawImage的第一参数不能为base64数据,如果你想用图片的话,请先new Image,将base64赋值给这张图片,然后再进行绘制噢~不过切记drawImage要写在实例后的image的onload事件后噢
12、canvas涂鸦时候出现绘制偏离是怎么回事?
答案就是因为canvas设置了百分比宽高,canvas里面也会继承此百分比,所以导致画笔和绘制相关都会出错,所以我强烈建议大家对canvas设置实际宽高像素!
---有问题的话持续更新,欢迎提问---