【Web技术】774- 基于canvas完成图片裁剪工具

前端自习课

共 6701字,需浏览 14分钟

 · 2020-11-15

前言

本文是基于canvas去实现图片裁剪工具。因为canvas代码还是比较长的,尽量写思路,完整代码已放在github上。


canvas模糊问题

这个是写canvas必定接触的问题,网上关于这个的答案也到处都是,就不详细介绍了。

因为canvas不是矢量图,在Retina屏下,浏览器用多个像素点去渲染一个像素,导致canvas最后呈现出模糊问题。

解决方案:

  • 获取window.devicePixelRatio设备的物理像素分辨率与CSS像素分辨率的比值。

  • canvas context有个属性backingStorePixelRatio表示渲染canvas之前会用几个像素来存储画布信息。不过这个只在某些浏览器上有,例如safari

  • 通过设置canvas.width/heightcanvas.style.width/heightcanvas进行缩放处理,比例为devicePixelRatio/backingStorePixelRatio(ratio)。(canvas.width/height表示画布实际大小,而canvas.style.width/height表示在浏览器上渲染结果大小)

  • 最后再通过context.scale(ratio, ratio)canvas进行处理,修复他的呈现效果

如果用typescript的话,会报backingStorePixelRatio不存在错误,加上一个类型定义文件解决。

export const getPixelRatio = (context: CanvasRenderingContext2D) => {  const backingStore =    context.backingStorePixelRatio ||    context.webkitBackingStorePixelRatio ||    context.mozBackingStorePixelRatio ||    context.msBackingStorePixelRatio ||    context.oBackingStorePixelRatio || 1;  return (window.devicePixelRatio || 1) / backingStore;};const calcCanvasSize = () => {    //...dosth.    canvasRef.current.style.width = `${canvasWidth}px`;    canvasRef.current.style.height = `${canvasHeight}px`;    canvasRef.current.width = canvasWidth * ratio;    canvasRef.current.height = canvasHeight * ratio;    ctx.scale(ratio, ratio);};

给canvas画上img

这个其实就是,通过input获取到本地图片文件,通过window.URL.createObjectURL获取到DOMString,将其作为imgsrc。通过ctx.drawImage将图片绘画到canvas上。

因为对于图片裁剪工具而言,img是应该绘画在最底层,所以需要通过globalCompositeOperation,将其绘画在底层。(globalCompositeOperation表示如何将一个源(新的)图像绘制到目标(已有)的图像上。)

const handleChoiseImg = () => {    if (createURL) {      window.URL.revokeObjectURL(createURL);    };
createURL = window.URL.createObjectURL(inputRef.current!.files![0]); img = new Image(); img.onload = () => { //initImageCanvas(img); 这个函数我是去获取img应该缩小比例和缩小宽高 // calcCanvasSize(); 这个我是去获取canvas应该呈现的size drawImage(); //绘画img }; img.src = createURL;};
const drawImage = () => { // todo sth. ctx.save(); ctx.globalCompositeOperation = 'destination-over'; // ctx.translate(canvasWidth / 2, canvasHeight / 2); // ctx.rotate(Math.PI / 180 * rotate); // if (rotate % 180 !== 0) { // [canvasWidth, canvasHeight] = [canvasHeight, canvasWidth]; // }; // ctx.translate(-canvasWidth / 2, - canvasHeight / 2); ctx.drawImage( img, (canvasWidth - scaleImgWidth) / 2, (canvasHeight - scaleImgHeight) / 2, scaleImgWidth, scaleImgHeight ); // canvasWidth/Height表示canvas的宽高(style),scaleImgWidth/Height表示图片缩放后的宽高 ctx.restore();};

蒙层&选中框

蒙层绘制

还是利用globalCompositeOperation将其绘画在已有图像的上方。

const drawCover = () => {    ctx.save();    ctx.fillStyle = 'rgba(0,0,0,0.5)';    ctx.fillRect(0, 0, canvasSize.width, canvasSize.height);    ctx.globalCompositeOperation = 'source-atop';    ctx.restore();};

选中框绘制

其实选中框,就是通过clearRect清除某个区域的蒙层,然后绘画自己的框框style,最后将img绘画在底层。

canvas的动画都是一帧一帧绘画出来的,选中框的拖动过程,其实就是不断去clearRect整个canvas,然后重新走上面的流程,即重新绘画的过程。

const drawSelect = (x: number, y: number, w: number, h: number) => {    ctx.clearRect(0, 0, canvasSize.width, canvasSize.height);    //清空整个canvas    drawCover();    //绘画蒙层    ctx.save();    ctx.clearRect(x, y, w, h);    //清空选中区域    ctx.strokeStyle = '#5696f8';    ctx.strokeRect(x, y, w, h);    // 画选中框    // todo sth. 给选中框加一些style    ctx.restore();    drawImage();    // 绘画图片};

选中框拖拽拉伸&边界处理

选中框拖拽拉伸就是,对mouse事件的处理,在mouseDown的时候,给其一个标志符,在mouseMove进行选中框不断刷新绘制,在mouseUp取消标志符(这个事件可以给外面容器)。

边界处理,就是对mouseMove处理过的选中框位置进行处理判断,若超出边界,则修复他。就是对offsetXoffsetY进行处理,然后在不同方向上去判断如何修改选中框,由于代码量比较大,完整可去github上看。

效果图:


图片旋转处理

canvas旋转中心是以左上角为中心,如果直接调用rotate,那么结果肯定不是我们想要的结果。那么就利用到了translate去移动canvas到中心点,然后再调用rotate旋转,旋转结束后再利用translatecanvas移回他的位置。

唯一的问题就是,弄清rotate后,你再translate平移canvas这个时候的x、y的值。

我这边对于图片裁剪工具的处理是,旋转后,去修改canvaswidth/height&style width/height。这个时候,canvas是旋转了,但是image重新绘画的时候,也要绘画旋转后的图,那么就利用上方讲的方法去旋转绘画。

还有就是别忘记通过save & restore去保存和恢复绘图状态。

const drawImage = () => {    // todo sth.    ctx.save();    ctx.globalCompositeOperation = 'destination-over';    ctx.translate(canvasWidth / 2, canvasHeight / 2);    ctx.rotate(Math.PI / 180 * rotate);    if (rotate % 180 !== 0) {      [canvasWidth, canvasHeight] = [canvasHeight, canvasWidth];    };    ctx.translate(-canvasWidth / 2, - canvasHeight / 2);    ctx.drawImage(      img,      (canvasWidth - scaleImgWidth) / 2, (canvasHeight - scaleImgHeight) / 2,      scaleImgWidth, scaleImgHeight    );    ctx.restore();};
效果图:


图片缩放处理

scale也是以左上角为缩放中心,然后如果缩放的话也需要save & restore,不然会对后续操作进行影响。

不过,我这里没有采用scale,而是手动修改图片缩放比例,然后重新得到scaleImgWidthscaleImgHeight,在去调用drawImage。因为代码上是将其显示在中心,所以就可以直接修改后调用。

// 修改 scaleImg 得到scaleImgWidth & scaleImgHeightctx.drawImage(  img,  (canvasWidth - scaleImgWidth) / 2, (canvasHeight - scaleImgHeight) / 2,  scaleImgWidth, scaleImgHeight);
效果图:


图片灰度处理

灰度处理就是通过getImageData获取canvasImageData即像素数据,可以对像素数据进行处理。然后再将这个处理后的像素数据,重新通过putImageData放回到canvas上。

像素数据,对于每个像素都有四个方面的信息,分别是RedGreenBlueAlpha

灰度处理公式还是挺多的,我这边就采用(R + 2G + B) >> 2

const imgData = ctx.getImageData(0, 0, canvasSize.width * ratio, canvasSize.height * ratio);getGrayscaleData(imgData);ctx.putImageData(imgData, 0, 0);

除此之外,还可以做很多类似的处理,比如,对比色处理,颜色选择器等等。

效果图:


实时显示截选的图片

如果仅仅是去截选canvas目前显示的部分,是不太友好的。应该是对应到原始图片的相应位置,去截选这个位置的图片才是比较友好的。

处理思路:

  • 新创建一个canvas,将img完整绘画在上面,并且完成旋转问题

  • 通过选中框的x y w h的值,还有img width/heightcanvas width/height的值,得到对应原始图片的截选部分的x y

  • 通过getImageData得到ImageData,并判断是否需要灰度处理

  • 然后重新修改上面创建的canvaswidth/height为选中图片部分的putW putH

  • ImageData通过putImageData放入canvas中  通过toBlob获取到blob

  • 最后通过window.URL.createObjectURL获取到DOMString

export const getPhotoData = () => {    const canvas = document.createElement('canvas');    const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;    // todo canvas处理
ctx.drawImage(img, 0, 0, imgWidth, imgHeight); // 处理获得putX putY putW putH const imgData = ctx.getImageData(putX, putY, putW, putH); if (grayscale) { //灰度处理 getGrayscaleData(imgData); }; canvas.width = putW; canvas.height = putH;
ctx.putImageData(imgData, 0, 0); return new Promise(res => { canvas.toBlob(e => res(e)); });};
const cancelChangeSelect = async () => { // todo sth. dataUrl && (window.URL.revokeObjectURL(dataUrl)); const blob = await getPhotoData() as Blob; const newDataUrl = window.URL.createObjectURL(blob); setDataUrl(newDataUrl); // todo sth.};// 省去不关键代码

效果图:


下载截选图片

这个其实上面已经写的差不多了,获取到了dataUrl后,将其作为a标签的href,下载就完事儿了。(当然还有很多其他下载方式,就不一一列举了)


源自:https://segmentfault.com/a/1190000015288700

声明:文章著作权归作者所有,如有侵权,请联系小编删除。

1. JavaScript 重温系列(22篇全)
2. ECMAScript 重温系列(10篇全)
3. JavaScript设计模式 重温系列(9篇全)
4. 正则 / 框架 / 算法等 重温系列(16篇全)
5. Webpack4 入门(上)|| Webpack4 入门(下)
6. MobX 入门(上) ||  MobX 入门(下)
7. 80+篇原创系列汇总

回复“加群”与大佬们一起交流学习~

点击“阅读原文”查看 80+ 篇原创文章

浏览 50
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报