手写裁剪图片的方案以及遇到问题的解决历程

1,099 阅读5分钟

起因

基于一个复杂的图片预览的弹窗之上新增一个裁剪功能,直接用新的插件改掉原来的功能又很复杂而且操作方式与需求不符。因此需要自定义一个简单的裁剪功能。以canvas画布元素与图片标签的位置关系和大小进行裁剪。

canvas方案

流程

  • 首先创建canvas对像
  • 指定canvas的高度宽度(非style的宽高)
  • 计算原图需要剪切的相关参数
  • 通过drawImage方法将需要的部分画上去
  • 通过toDataURL方法获取图片base64的值
  • 完成,后续可对base64值进行其他操作。
    //本地或线上图片地址
    const SRC = "./aa.jpg"
    const canvas = document.getElementById('canvas-cut')
    const cut = canvas.getContext('2d')
    const img = new Image()
    img.src = SRC
    //处理toDataURL遇跨域资源导致的报错
    img.crossOrigin = 'Anonymous'
    img.onload = function() {
        cut.drawImage(img,295,40,100,100,0 ,0,100,100)
        var imgbase64 = canvas.toDataURL("image/png")
        //base64资源
        console.log(imgbase64)
    };

drawImage参数

drawImage 方法允许在 canvas 中插入其他图像(img和canvas元素)

以左上角为原点x,y轴的距离

    //参数有三种传值方式
    drawImage(image, dx, dy)   
    drawImage(image, dx, dy, dw, dh)
    //dx,dy 在画布上裁剪后图像放置的 x,y坐标位置
    //sw和sh为在原图中需要截取的宽高,最后会以缩放的方式以dw,dh的大小展示在canvas中
    drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)

draw.png

适配原图的缩放旋转

当原图存在缩放和旋转的情况,裁剪也需要进行缩放旋转

旋转

canvas的旋转也是以左上角有为原点为旋转,旋转90度之后,则内容为空白了,因此需要translate属性移动一个距离

cut.rotate(Math.PI / 2)
cut.translate(0, -1 * canvas.height)

rotate.png

//顺时针与逆时针旋转-270~270之间
// rationNum = (旋转角度/90)%4
cut.rotate(Math.PI * rationNum / 2)
switch (rationNum) {
    case -1:
    case 3:
      cut.translate(-1 * canvas.width, 0)
      break
    case 1:
    case -3:
      cut.translate(0, -1 * canvas.height)
      break
    case -2:
    case 2:
      cut.translate(-1 * canvas.width, -1 * canvas.height)
      break
  }
}

rotatiom.png

按照上图旋转之后,画布与参照图片的关系,在实际裁剪的图的裁剪方位将会有变化(原点变化了) 这里需要根据canvas与展示的图片的位置关系,推算出裁剪原图和canvas的位置关系即sx,sy的值

aaa1.png

//drawLocal为裁剪的方位sx,sy最后还需要乘以缩放比例
//注意:我这里的canvas是正方形的所以width和height值一样,此处都用canvas.width
//drawLocal在原图上面裁剪的起始位sx,sy的值
switch (rationNum) {
  case -1:
  case 3:
    drawLocal = {
      x: img.width - (canvasXY.y - imgXY.y) - canvas.width,
      y: canvasXY.x - imgXY.x
    }
    break
  case 1:
  case -3:
    drawLocal = {
      x: canvasXY.y - imgXY.y,
      y: img.height - (canvasXY.x - imgXY.x) - canvas.width
    }
    break
  case -2:
  case 2:
    drawLocal = {
      x: img.width - (canvasXY.x - imgXY.x) - canvas.width,
      y: img.height - (canvasXY.y - imgXY.y) - canvas.width
    }
    break
}

aa (1).png

缩放

实际中,看到的是一张图片,裁剪的是以new Image()新生的图片,因此所有的距离必须以画布与展示的图片计算坐标数据,然后再以缩放比例进行计算

const imgScale = img.style.transform ? img.style.transform.match(/scale\((\S*)\)/)[1] : 1
const scaleNum = originImg.width / (img.width * imgScale)

图片有两张:一张可以缩放旋转的图,一张是实际截取内容的图片

//最后大致如下
//注意:这里canvas为一个正方形
cut.drawImage(
    originImg,
    drawLocal.x * ration,
    drawLocal.y * ration,
    canvas.width * ration,
    canvas.width * ration,
    0,
    0,
    canvas.width,
    canvas.width
)

内部图片不支持跨域与canvas crossOrigin属性的冲突

所谓的跨域是因为浏览器的同源策略而引起的。(协议,域名,端口号)

但是img和link标签只能实现单向通信,即只能从客户端向服务器传递数据。img获取图片进行加载,link是获取样式表加载,而script返回的是javascript代码执行。因此,script,img,link标签不受跨域影响。 但是canvas需要对裁剪的图片进行可跨域的设置

originImg.setAttribute('crossOrigin', 'anonymous') 因此会出现,页面明明有图片,但是设置该属性就回401(设置为允许跨域之后会图片的请求会不带上cookie) 解决方案就是使用ajax发起请求获取图片资源然后再设置给img(由于img标签不受跨域的影响,当用ajax请求是很可能出现跨域的现象,这里需要后端设置一下

//data 拿到资源然后转位base64格式
const reader = new FileReader()
reader.readAsDataURL(data)
reader.onload = ({ target }) => {
  callback((target as FileReader).result as string)
}

重置canvas画布内容

  • 使用: context.clearRect(0, 0, canvas.width, canvas.height)

    这是清除整个canvas的最快和最具描述性的方法。

  • 不要使用: canvas.width = canvas.width

    重置canvas.width重置所有canvas状态(例如,转换,lineWidth,strokeStyle等),它非常缓慢,不适用于所有浏览器,并且不描述你实际尝试的内容去做(自己赋值自己???重点是typescript也无法解析)

关于git

由于多次的尝试以及本地有代理,之前跨域请求在打包发布至测试环境才会出现跨域的问题,产生很多git log,一把辛酸泪。所以这里需要git合并多个commit,不然不好看

u=1247095003,2276844947&fm=26&gp=0.jpg
//找到最开始的commit id
git log
//软重置
git reset --soft 1111111111
//生成一次新的commit
git commit -m'XXX功能'
git push -f

最后

总结一下,canvas裁剪会遇到图片资源跨域的问题,适配图片的旋转缩放,不同的预览图片的插件,旋转缩放的方式也不同,但最终是需要计算出比例即可。。。。。

希望遇到的每一个坑都是一次进步~