一个可以用来截图的 React 组件

3,016 阅读6分钟

灵感

起初只是和同事在去吃路的路上, 互相吹*, 扯一些有的没得, 然后说到了马赛克, 探讨了一下马赛克的实现, 觉得还蛮有意思的, 感觉可以实现一波, 后面我觉得只实现马赛克功能又太单调, 就像做一个类似微信截图的功能.

关于组件

实现效果

组件

github

基本效果都是参照微信截图去做的, 但是还是不一样, 微信截图的功能还是比较通用, 自己还有功能没实现, 后面会说明明细

基本原理

canvas + image 去实现的, 说实话, canvas我用的也不是很熟悉, 只了解基本的api, 但是基本思路还是明白的, 前面截图大小位置的拖动就是操作dom, 后面对截图的标注之类的都是canvas相关的操作, 一步步实现

实现细节

最好先看下 github 的 demo, 不然解释起来比较麻烦

打开一个图片

其实就是展示一个image, 浏览器如何展示这个image参考手机打开一个图片吧, 举个例子, 1980 * 720 的屏幕, 打开 720 * 460 的图片, 图片大小保持不变, 上下左右留白

如果打开一个 2800 * 1000 的图片, 应该是宽度撑满, 类似这种

反过来, 高度撑满

逻辑大概这样

const t = image.width / image.height;
if (height < image.height || width < image.width) {
  const ws = image.width / width;
  const hs = image.height / height;

  if (ws <= hs) {
    return [Math.floor(height * t), Math.floor(height)];
  } else {
    return [Math.floor(width), Math.floor(width / t)];
  }
}

return [Math.floor(image.width), Math.floor(image.height)];

需要注意的是, canvaswidthstyle.width是两个东西, 我会保留两者的比例, 后面很多操作都需要这个比例

选取截图范围

确定一个矩形的返回, 只需要知道两个信息, 一个是top left, 一个是 width height, 拖动的时候是分两种情况的, 打个比方, 一个是从左上向右下拖动, top left不变, 只改变width height, 一种是从右下向左上拖动, top left改变, width height 改变, 判断第一个点和拖动的点就能区分这两种情况.然后根据top left width heightcanvas上通过drawImage渲染出截取范围的图片. 还需要注意的是, 也需要给canvas绑定mousemove事件,不然鼠标飘到canvas上的时候就不能拖动了.

此处有个待优化点, 就是当图片尺寸过大时, 拖动不停的drawImage会有一丢丢的卡顿, 这块我后面想优化下, 大概改成这样: 拖动的时候只是控制矩形的border,mouseup的时候再去在canvas上绘画.

拖动点控制大小

截取范围确定后, 可以通过边角和中间的八个点来控制大小, 这块也是分两种情况的:

  1. 拖动边角的点的行为基本上是一致的
  2. 拖动中间的点的行为差不多, 但有一些差别, 就是控制的是宽度还是高度的差别

这块我的处理是保存边角的四个点, 拖动边角的行为本质上和我们选取截图范围的行为是一致的, 因为我保存了四个点, 只要我知道对角的两个点的坐标就能知道怎么去控制这个矩形的变化.

拖动中间的点和边角是很像的, 基本上就是高度不变, 宽度改变, 和宽度改变, 高度不变

拖动改变位置

这个更简单, 就是根据坐标变化计算拖动的距离, 需要注意的就是边界的判定, 因为你不能拖动到外面去了

放大镜的效果

记录鼠标的位置, 根据宽高drawImage, 需要乘以上面提到的比例, 也需要注意边界的判定.

标注矩形和圆形

这两个放一起, 因为这两个的行为基本上是一致的, 只是形状不同, 刚开始的想法是, 拖动的时候不停clearRect, 再重新drawImage, 这里又有上文提到的问题了, 这里需要把绘图动作存起来, 不然矩形没发保存, 绘图动作越存越多, 拖动的时候会越来越卡, 后面我做了优化, 拖动的时候, 展示的其实是个svg, 等到mouseup的时候再去画 矩形|圆形.

这块有个问题, 感觉线条宽度在svgcanvas里面的表现形式有点不一样, 这个后面还需要优化

画线

这个比较简单, 记住上一个点就好了, 具体不细说了

马赛克

这个也是蛮有意思的, 因为早就有了思路, 做起来也是比较简单. 打个比方, 画图长宽都为100, 马赛克大小为 10, 我点了坐标(30, 40), 换算就是坐标为(3, 4)的马赛克

// 马赛克大小
const size = 10;
const w = 100;
const h = 100;
const loc = { x: 33, y: 33};
const row = Math.floor(loc.x / size);
const col = Math.floor(loc.y / size);
const index = col * w + row;
const locX = index % w - 1;
const locY = Math.floor(index / w);
// index 是 imageData 的第几个点, 用来填充这个方块
const index = locY * size * w + locX * size;
const r = imgData.data[dataIndex * 4];
const g = imgData.data[dataIndex * 4 + 1];
const b = imgData.data[dataIndex * 4 + 2];
const color = rgb2hex(r, g, b);
context.fillRect(locX * size, locY * size, size, size);

上面是比较简单的实现, 实际需要考虑画笔的宽度啥的

打马赛克, 目前比例是 1 的时候正常, 不是 1 的时候, 有点问题, 因为计算的比例一般不可能为整数, 就会导致误差, 这块我是打算重写, 换种方式去处理, 目前的处理方式还是觉得不好

todo: 箭头

这个有思路, 但暂时没做了, 以前做过移动端对图片的旋转放大, 两个行为基本一致, 想法也是通过 svg 进行处理, 这个后面看情况补上

todo: 标注文字

这个也是有思路, 没做, 感觉还是用操作dom处理比较方便

撤销

撤销我刚开始想偷个懒, 这样处理的, getImageData => 保存 => 撤销 => putImageData. 后面为什么改了呢? 一是putImageData性能不好, 二是, 如果是个 8000 * 6000 的图片, 每个操作我都把这些像素信息存下来, 感觉还是很恐怖的....

后面改成只保存绘画操作, 撤销想当于去掉最后一步操作, 把前面的绘画操作执行一遍就好了

下载

很简单, 转成base64, data-url 的形式, 不做过多说明

拷贝

这个比较麻烦, 本来是想做成拷贝的粘贴板上的, 网上找了半天资料, 最后只做成了选中效果...没啥用的感觉, 目前只是做成触发回调的形式.

浏览器拷贝图片到粘贴板应该还是没法实现, 不知道微信的是怎么处理的, 有了解的同学可以帮忙科普下

做不了: 拖动

微信的拖动这块感觉还是比较麻烦, 矩形 | 圆 | 线条, 有点思路, 但感觉很麻烦, 我选择放弃思考, 有好的点子的同学可以帮忙提点建议

总结

大概这么多, 涉及到细节基本都一笔带过了, 总体还是比较有意思的一个组件, 目前还有很多优化的点, 当时只是打草稿写着玩的, 所以现在代码还有点乱.

内容比较简单, 后面看情况补充(有人看的话...)