林大大又来更新啦~
这次主要做的是关于canvas图像绘制的部分,要实现的功能主要是绘制、对canvas的放大,缩小,拖动以及截图
本篇重点:图像的截图所使用的矩形框是怎么画出来的呢?想使用的童鞋参考我另一篇博客~
【JS】原生js实现矩形框的绘制/拖动/缩放
那我们先来大概了解下canvas:
this.canvas = document.getElementById('canvas') // 画布对象
this.context = this.canvas.getContext('2d') // 画布显示二维图片
// 注意设置百分比的话,父盒子要设置实际宽高
width: 200px 或者 100%
height: 200px 或者 100%
宽高是必须加上的,当然你也可以选择动态宽高,可以设置宽高百分比喔,但是我建议还是尽量设置实际像素~
canvas用于在页面上绘制图像(可以是自己的图片,也可以绘制自定义矢量图形:矩形等),其实我们不用担心大量的重绘canvas,现在浏览器完全扛得住这压力,非常流畅丝滑
下面我来大概讲解下绘制图像,绘制自定义矢量图形其他博客上有很多,我这里就先省略了
截图:
首先整个灰色部分都是canvas(上面操作栏不是canvas喔),我们可以看到我们所使用的图是位于左上角的,canvas绘制是以左上角(0,0)为原始点的
可以说drawImage方法是绘制图像的独有方法,只有调用了这个方法才能进行绘制,那在绘制之前应该注意些什么呢?
首先要将canvas里进行清除(例如你之前已经绘制过了,想要重新绘制,则应该先清除)
this.context.clearRect(0, 0, width, height);
这里的第一二参数分别为你要清除的区域起始点的x,y轴坐标,第三四参数分别为你要清除的区域的宽高,我们就只需要写入 0, 0, width, height 来清除整个canvas区域
在绘制前我们得对要绘制的图片有了解,比如图片宽高,是否跨域(这是个重点,后面我讲一下,很常遇到),并且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比例
}
上面已经将图像的各部分参数计算出来了,那现在开始绘制图像了
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);
我们一般都想对图片进行完全展示,那么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 // 要使用的图像的宽度、高度
)
}
随时监听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() // 重新绘制图片
},
/**
* 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
}
},
添加鼠标监听,切记要在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
};
}
添加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
其实这个单独做起来也不难,一个canvas搞定很多其他博客有。但是要是和我上面代码结合的话,就比较麻烦了,所以你要用我上面的放大缩小拖拽,然后再想弄截图,就下面评论或私聊我吧,毕竟截图更麻烦,博客讲不清楚。。。
这种一般都是在图片服务器配置跨域参数解决
header("Access-Control-Allow-Origin: *"); // 任意域名
header("Access-Control-Allow-Origin: xxx"); // 指定域名
如果还是不行,则按照下面方式解决
const image = new Image()
image.crossOrigin = 'Anonymous'
image.src = xxx
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
}
我不建议这么转,虽然getImageData获取到所有像素点,并且可以修改,但是貌似不咋好写后续(实际就是我没弄出来,你们可以试试),我还是建议toBlob
我建议使用toBlob,这个网上有很多说法了,我就不一一解释了
如果你只是想要将涂鸦后的canvas转图片并且只展示的话,就可以getImageData配合putImageData,如果你想通过接口传给后端,那我建议你是用toBlob
Uncaught DOMException: Failed to execute 'getImageData' on 'CanvasRenderingContext2D': The canvas has been tainted by cross-origin data.
一般为上述描述,出现这种情况,你就得新开辟一个canvas,然后进行绘制,不在之前已经绘制过的canvas再次操作
document.createElement('canvas')
加上下面这几行阻止浏览器默认事件即可以解决卡顿导致的事件丢失问题,提升效率
document.onmousemove = (e) => {
e.preventDefault();
if (e.stopPropagation) {
e.stopPropagation();
} else {
e.cancelBubble = true;
}
}
有细心的童鞋会发现,对同一张图片进行多次赋相同的值,除了第一次onload生效外,后续的onload事件不会触发,这个时候与跨域无关,因为跨域会报错。我觉得可能是与浏览器的缓存机制有关,所以解决方式是后面加上时间戳,每次都去请求新的图片地址。
const image = new Image()
image.crossOrigin = 'Anonymous'
image.src = `${this.xxx}?t=${Date.now()}`
image.onload = () => {}
v-drag 为自定义拖拽指令,用于一般dom在父节点范围内拖动,想 了解的在我另一篇博客查看
vue学习(6)自定义指令详解及常见自定义指令
---有问题的话持续更新,欢迎提问---