这一路上真是艰难险阻呢
谈不上技术博客,只是简单谈一谈踩过的坑罢了。真是一阵难过呢呜呜呜
大方向
目前是有个这样的需求:
- 用户上传一张图片
- 拖动图片时,图片可以移动
- 拖动裁剪框时,裁剪框可以移动
- 点击放大、缩小,图片可以进行缩放
- 点击裁剪,图片可以进行裁剪并转成base64
需求很简单,但是实现起来还是有点麻烦。毕竟刚学前端,想用canvas自己实现一下,就当是练习了。
基本效果
很快裁剪框的基本效果就有了。也就是在进行拖动时,能顺着鼠标移动。主要用了鼠标的点击、移动事件实现的。
到这里很顺利,甚至不到10分钟就已经完全没问题了。本来以为图片的移动只需要如法炮制即可。结果却出现了闪屏现象。
踩坑
到这里开始,就一步一回头。基本就没什么顺利的解决过程了。主要是由于对浏览器、canvas的理解不足。
闪屏
这个闪屏问题,之前也简单查过资料。是由于渲染时经历了:有>无>有 这样的过程。毕竟浏览器是单线程的,所以需要等待。
审查代码时发现,由于方法没有提炼干净,导致每次拖动后重绘时,都会调用img的加载:img.onload()
。等img
加载完成后才进行渲染,这样不闪屏才怪呢。一顿噼里啪啦改写,问题就这样解决了。
范围限制
在拖动时,注意到图片能够拖到canvas外面,这显然不合理。也就是在拖动的时候要注意限制拖动量的范围。
这一步很简单,就是根据图片的宽高,和canvas的宽高,计算出图片的坐标范围,然后根据坐标范围来限制拖动的范围。
this.imgDivSetting.delx = Math.max(limiteW, Math.min(0, this.imgDivSetting.delx))
this.imgDivSetting.dely = Math.max(limiteH, Math.min(0, this.imgDivSetting.dely))
这时候的我还没想到,未来将在这里出大问题!
进行缩放
这一步也非常简单。但是由于对canvas的理解不足,也吃了大亏。
刚开始我决定用canvas的缩放方法scale(x,y)
。看了看文档,这个函数接收两个参数,第一个是x轴缩放比例,第二个是y轴缩放比例。
那无所谓,反正x\y缩放都是一样的,直接创建一个变量scale
,用来记录缩放比例,然后每次缩放时,直接scale+=0.1
,然后ctx.scale(scale,scale)
。
不出意外的话,这时候就该出意外了。点击一次的时候确实是放大了。但我注意到一个问题:
当点击多次放大后,在点击缩小。图片依然是一个放大效果。这时候没有深入研究,却百思不得其解。索性就不用scale方法了,直接在绘制的时候,用ctx.drawImage(img,x,y,width*scale,height*scale)
实现放大缩小。
到这一步,虽然并不理解,但依然实现了放大缩小的功能
进行旋转
噩梦开始的地方
旋转的时候,我直接用canvas的rotate(angle)
方法,然后ctx.drawImage(img,x,y,width,height)
。
旋转后拖动效果异常
乍一看并没有什么问题。但是旋转后,再进行拖动的时候,图片会乱动:
你向上托,图片往右走。你向右托,图片往下走。
此时由于对canvas的理解不足,只是单纯的以为拖动时,还需要对图片进行旋转。于是就发现,拖动后,图片开始疯狂闪烁。
原因
仔细研究发现,闪烁的原因是在拖动后,图片又进行了旋转。由于拖动事件频繁触发,导致图片飞快的旋转。
研究了文档后,才知道,rotate方法会改变坐标系,导致坐标位置发生变化。也就是
在调用rotate(90)时,坐标旋转了90度,坐标位置也发生了变化。
再次调用rotate(90)时,坐标又旋转了90度,此时坐标旋转了180度。
如果把这个旋转放到拖动事件里,图片的坐标轴就会飞速转换,自然就闪烁了。
没错 坐标轴不规则放大也是这个原因
当点击了放大后,此时scale为1.1。如果再次点击放大,scale变成了1.2。此时看看怎么个逻辑呢:
ctx?.scale(1.1); // 相比于1放大了1.1倍 当前为1.1
ctx?.scale(1.2);// 相比于1.1放大了1.2倍 当前为1.1*1.2=1.32
如果这时候点击缩小,此时scale变成了1.1,则:
ctx?.scale(1.1); // 相比于1放大了1.1倍 当前为1.1
ctx?.scale(1.2);// 相比于1.1放大了1.2倍 当前为1.1*1.2=1.32
// 点击了缩小
ctx?.scale(1.1); // 相比于1.32放大了1.1倍,此时为1.452
于是虽然点击了缩小,但实际上坐标轴依然放大。
解决方案
我选择将拖动时的坐标偏移量、坐标位置保存下来,在渲染图片之前进行计算,在旋转。这里用到了这样的公式:
x' = x * cos(angle) - y * sin(angle)
y' = x * sin(angle) + y * cos(angle)
const [dx, dy] = this.getRotate([this.imgDivSetting.delx, this.imgDivSetting.dely]);
this.ctx?.drawImage(this.img!, dx, dy, width * this.imgSetting.scale, height * this.imgSetting.scale)
旋转后保证图片在canvas中
到这里,不管是学习还是踩坑,已经过了一天时间了。本打算一鼓作气完成最后一点,结果当天无论如何都无法实现这个效果。第三天才完成。
首先先看一下旋转的坐标轴吧:
正常坐标轴:
旋转坐标轴:
灰色的部分就是canvas了。可以看到,当旋转后,图片的坐标轴也跟着旋转了。导致图片完全渲染在canvas外面。由于上一步添加了限制,导致无论如何都无法渲染在canvas里面
原因
原因其实已经说明白了,就是旋转坐标轴导致图片坐标轴也跟着旋转了,图片完全渲染在canvas外面。
解决方案(不完整)
通过查阅资料,发现有这用解决方案:
- 将坐标移动到图片中心点
- 将图片旋转
- 将坐标移动到原点
没错,这次我又不甚了解,只是简单想了想,觉得有道理就这么做了。在这里先劝大家千万千万不要学我,会付出惨重的代价
来看代码:
this.ctx?.translate(this.img!.width / 2, this.img!.height / 2)
this.ctx?.rotate(this.imgSetting.rotate / 180 * Math.PI)
this.ctx?.translate(-this.img!.width / 2, -this.img!.height / 2)
这时我理解错了,本以为移动的渲染时的图片。还以为是代码错了:
但修改之后反而达不到效果(虽然没修改也没达到效果就是了)就这样贴上去了。
可是,在之后就又出问题了
旋转后限制区域错误
我注意到旋转90度后,当图片在坐标[0,0]的时候,实际上在canvas的右边:
这个是刚刚的理论无论如何都无法解决的。并且如果这个不解决,那我就没办法限制住图片的位置了。
查找原因
通过控制台,深入思考、计算了一下,此时图片的大小为:1920x1080。漏出来大约92。因此,此时坐标轴在[988,420]的位置。参考canvas的大小为:512x512。
注意到有一个莫名其妙的偏移量[988, 420]、[988, -420]
我突然发现:
988 = 1920/2+1080/2-512
420 = 1920/2+1080/2-1080
-420 = 1920/2+1080/2-1920
而÷2的操作只有在移动坐标轴的时候出现过,因此肯定是那里出问题了!
原因
参考刚刚的算式。我突然想到:会不会translate方法实际上是移动的坐标轴呢?
经过控制台测试,发现果然如此!
解决
其实只需要为90度添加一个新的限制条件就能解决了
let limiteW: number = -this.img!.width * this.imgSetting.scale + this.imgSetting.width;
let limiteH: number = -this.img!.height * this.imgSetting.scale + this.imgSetting.height;
if (this.imgSetting.rotate % 180 == 0) {
this.imgDivSetting.delx = Math.max(limiteW, Math.min(0, this.imgDivSetting.delx))
this.imgDivSetting.dely = Math.max(limiteH, Math.min(0, this.imgDivSetting.dely))
} else {
const delta = -this.img!.width / 2 - this.img!.height / 2
this.imgDivSetting.delx = Math.max(delta + this.imgSetting.height, Math.min(this.img!.height * this.imgSetting.scale + delta, this.imgDivSetting.delx))
this.imgDivSetting.dely = Math.max(delta + this.imgSetting.height, Math.min(this.img!.width * this.imgSetting.scale + delta, this.imgDivSetting.dely))
}
至此,问题完美解决
总结
scale()
,translate()
方法操作的都是画布的坐标轴,而不是图片的坐标轴。现在想来还真是理所当然- 旋转后的拖动与缩放要按新的坐标轴来算
- 最重要的一点:学习一定要深入思考,不要只看代码。不能浅尝辄止!