前言
本文是基于canvas
去实现图片裁剪工具。因为canvas
代码还是比较长的,尽量写思路,完整代码已放在github上。
canvas模糊问题
这个是写canvas
必定接触的问题,网上关于这个的答案也到处都是,就不详细介绍了。
因为canvas
不是矢量图,在Retina
屏下,浏览器用多个像素点去渲染一个像素,导致canvas
最后呈现出模糊问题。
解决方案:
- 获取
window.devicePixelRatio
设备的物理像素分辨率与CSS
像素分辨率的比值。 canvas context
有个属性backingStorePixelRatio
表示渲染canvas
之前会用几个像素来存储画布信息。不过这个只在某些浏览器上有,例如safari
- 通过设置
canvas.width/height
和canvas.style.width/height
对canvas
进行缩放处理,比例为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
,将其作为img
的src
。通过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
处理过的选中框位置进行处理判断,若超出边界,则修复他。就是对offsetX
和offsetY
进行处理,然后在不同方向上去判断如何修改选中框,由于代码量比较大,完整可去github上看。
效果图:
图片旋转处理
canvas
旋转中心是以左上角为中心,如果直接调用rotate
,那么结果肯定不是我们想要的结果。那么就利用到了translate
去移动canvas
到中心点,然后再调用rotate
旋转,旋转结束后再利用translate
将canvas
移回他的位置。
唯一的问题就是,弄清rotate
后,你再translate
平移canvas
这个时候的x、y
的值。
我这边对于图片裁剪工具的处理是,旋转后,去修改canvas
的width/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();
};
代码语言:javascript复制效果图:
图片缩放处理
scale
也是以左上角为缩放中心,然后如果缩放的话也需要save & restore
,不然会对后续操作进行影响。
不过,我这里没有采用scale
,而是手动修改图片缩放比例,然后重新得到scaleImgWidth
和scaleImgHeight
,在去调用drawImage
。因为代码上是将其显示在中心,所以就可以直接修改后调用。
// 修改 scaleImg 得到scaleImgWidth & scaleImgHeight
ctx.drawImage(
img,
(canvasWidth - scaleImgWidth) / 2, (canvasHeight - scaleImgHeight) / 2,
scaleImgWidth, scaleImgHeight
);
代码语言:javascript复制效果图:
图片灰度处理
灰度处理就是通过getImageData
获取canvas
的ImageData
即像素数据,可以对像素数据进行处理。然后再将这个处理后的像素数据,重新通过putImageData
放回到canvas
上。
像素数据,对于每个像素都有四个方面的信息,分别是Red
,Green
,Blue
,Alpha
。
灰度处理公式还是挺多的,我这边就采用(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/height
和canvas width/height
的值,得到对应原始图片的截选部分的x y
- 通过
getImageData
得到ImageData
,并判断是否需要灰度处理 - 然后重新修改上面创建的
canvas
的width/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
声明:文章著作权归作者所有,如有侵权,请联系小编删除。