【前端】关于Canvas的那些坑

这一路上真是艰难险阻呢

谈不上技术博客,只是简单谈一谈踩过的坑罢了。真是一阵难过呢呜呜呜

大方向

目前是有个这样的需求:

  1. 用户上传一张图片
  2. 拖动图片时,图片可以移动
  3. 拖动裁剪框时,裁剪框可以移动
  4. 点击放大、缩小,图片可以进行缩放
  5. 点击裁剪,图片可以进行裁剪并转成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外面。

解决方案(不完整)

通过查阅资料,发现有这用解决方案:

  1. 坐标移动到图片中心点
  2. 将图片旋转
  3. 坐标移动到原点

没错,这次我又不甚了解,只是简单想了想,觉得有道理就这么做了。在这里先劝大家千万千万不要学我,会付出惨重的代价

来看代码:

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)

这时我理解错了,本以为移动的渲染时的图片。还以为是代码错了:
123

但修改之后反而达不到效果(虽然没修改也没达到效果就是了)就这样贴上去了。

可是,在之后就又出问题了

旋转后限制区域错误

我注意到旋转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()方法操作的都是画布的坐标轴,而不是图片的坐标轴。现在想来还真是理所当然
  • 旋转后的拖动与缩放要按新的坐标轴来算
  • 最重要的一点:学习一定要深入思考,不要只看代码。不能浅尝辄止!
  • 30
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

飛_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值