前言
最近实现了一个“在线”批改的功能,其中包括截图,旋转,放大缩小,自适应等功能,调研了几个可支持截图的api,最终选择cropper来实现,以下介绍过程中总结的技巧以及避坑指南。
分以下几个阶段介绍
- cropper的简单介绍
- 项目背景 & 需要实现的主要技术点
- 实现过程中难点方案的技术实现设计
- 结语
cropper的简单介绍
调研了几个方向:原生canvas,fabric,cropper; 最后突出重围的即为cropper,cropper的主打点为截图,契合需求,git地址 || 官方demo 官方demo实现的比较可用了,直观的截图,旋转等都符合需求
react形式的基本使用
- yarn add react-cropper
- import Cropper from 'react-cropper' // 引入Cropper
<Cropper
src={src} // 截图的背景图url
className="cropper"
ref={cropperRef}
// Cropper.js options
viewMode={1}
zoomable={false}
aspectRatio={1}
guides={false}
preview=".cropper-preview"
/>
以上就是cropper的引入形式
项目背景 & 需要实现的主要技术点
项目需要做线上模拟老师的批改操作,为学生的卷子上框出错题,并填写错因等,拆开来说 需要基本的截图功能,并且卷子依赖学生上传,可能方向不对,所以需要调整,需要放大,缩小,旋转,自适应, 并且一张卷子可能会有多个错题,所以需要记录老师之前框选的错题位置,方便下次回看,所以需要实现主要点为
- 基本的截图
- 截图位置回显
- 放大缩小
- 旋转
- 自适应
实现过程中难点方案的技术实现设计
基本截图
cropper提供了 autoCrop 默认为true,会默认初始化出剪裁框, cropper提供了preview这个api,preview内放对应的选择器,就可以实现实时预览
<div className="cropper-container">
<Cropper
src={src}
className="cropper"
ref={cropperRef}
// Cropper.js options
viewMode={1}
zoomable={false}
aspectRatio={1}
guides={false}
preview=".cropper-preview"
/>
</div>
<div className="preview-container">
<div className="cropper-preview" />
</div>
但是对于需求开始不仅仅是简单的preview,而是要根据截图的错题后边附上对应的错因,所以需要自己实现类似于preview这个功能,自定义一个 “确认截图” 按钮,拿到截取后的图片
api: cropperRef.current.cropper.getCroppedCanvas().toDataURL('image/png') 这里拿到的是base64格式,
之后就可以写自己的内容
相对来说比较简单
截图位置回显
难点1:记录框题位置,可以通过切换框图的位置,展示对应的错题信息
难点2:需要记录当前截图背景的位置,因为可以旋转放大缩小等
方案1:
这里涉及到一个平铺位置信息的实现方案,我是直接无背景色定位一个截图位置的box,大小为截图背景图的大小
那实现截图框的话,需要框的大小,在图上的位置,即定位的left和top,查看cropper提供的api, cropperRef.current.cropper.getCropBoxData() 可以拿到截图后的位置信息,设计了一个存储格式
readyCutList = {
'当前截图的背景图片url': [{cutImg: "截取的片段url", height:'', left: '',top: '',width: ''}, {}]
}
以readyCutList的形式存储给后段,再次进入反显的时候,直接拿到readyCutList 根据key找到当前图片对于的位置数组,直接设置样式即可,点击对应的框拿到cutImg 回显错题信息
方案2:
记录截图的背景图的操作结果,需要记录 scale,rotate,with,height等信息,查看api发现没有一个是包含这里的所有信息的,之后通过 cropperRef.current.cropper.getData()拿到 rotate,通过 cropperRef.current.cropper.getCanvasData() 拿到width,height,left,top(距离截图盒子的位置)
cutPositionList = {
'当前截图的背景图片url': {canvasData: 'getCanvasData的信息',data: {getData()的信息}}
}
二次进入时,通过key值拿到对应信息,然后调用 setCanvasData('存储的CanvasData') setData('存储的data')
底图的放大缩小
这个功能说起来很简单,官方直接提供了cropperRef.current.cropper.zoom() 传正负值就可以实现放大缩小,看起来喜大普奔,but,称之为难点肯定有妖。实现后发现这个放大缩小的坐标中心为 正中心,即以图片的正中心来进行放大缩小,但是产品想要的效果是以左上为坐标中心,来进行操作,但是查看api,zoom这个方法并没有提供改变坐标中心的参数,之后尝试用js添加transform-origin更改,不起作用,后边埋头研究官方文档,看到了一个 zoomTo 后边可以设置坐标点,喜大普奔??,不急,这里还有坑,zoomTo(1),按我们正常理解是 到 100%,即原始大小,但是实际实验并不是,仔细研究文档,给出的解释是
canvasData.width === canvasData.naturalWidth
这个的意思是 缩放到图片的实际大小,解释说明一下:图片原本100x100, 我们给放到 50x50的div内,这个时候 zoomTo(1)会直接把图片设置为 100x100,这显然不是我们要的效果,所以需要手动计算,当前的大小和实际大小的比例,zoomTo这个比例 才是我们放大缩小的基础值,即
const nowSale = cropperRef.current.cropper.getCanvasData().width /
cropperRef.current.cropper.getCanvasData().naturalWidth;
即
放大: cropperRef.current.cropper.zoomTo(nowSale + 0.1, { x: 0, y: 0 });
缩小: cropperRef.current.cropper.zoomTo(nowSale - 0.1, { x: 0, y: 0 });
ps:实际 加减0.1不是很严谨,0.1是以基础值为1来做的,这个时候应该计算 当前基础的0.1对应的比例是 几,再去加减,emmm,,,,,
旋转
同样这个功能看起来比较简单,官方提供的是 cropperRef.current.cropper.roate(90)直接输入对应的度数即可旋转,but,是吧,,所以呢,这个的效果是以中心旋转,旋转90度后直接横向,想象一下 100x50的图片,旋转90度后,就变成上下超出屏幕,左右空白了,所以不能这样交付,所以要达成效果的是,旋转后自适应到当前存放截图区域的大小,这里边就需要判断是否溢出,溢出的话,按小的来渲染宽高
cropperRef.current.cropper.rotate(90);
// 旋转后重新设置位置 获取旋转后的宽高,然后重新设置位置
const getData = cropperRef.current.cropper.getCanvasData();
const {width, height} = getData
// 旋转后自适应到屏幕大小
const box = document.getElementsByClassName('cropper-container')[0]
const {offsetWidth, offsetHeight} = box
if (width > offsetWidth) {
cropperRef.current.cropper.setCanvasData({...getData, width: offsetWidth, height:height * offsetWidth / widht, left: 0, top: 0})
} else if (height > offsetHeight) {
cropperRef.current.cropper.setCanvasData({...getData, height: offsetHeight, width: width * offsetHeight / .height, left: 0, top: 0})
} else {
cropperRef.current.cropper.setCanvasData({...getData, left: 0, top: 0})
}
自适应
这个的点就是之前是按 zoomTo(1), 渲染的,当渲染大小和实际大小一样的时候,是没问题,但是如果不一致就发现问题了,在缩放的时候讲过,需要计算比例,按现在比例去做自适应
结语
到此,遇到的稍微复杂的地方就讲完了,本篇文章更像是一个踩坑记录,一般用cropper的只需要他的截图,不涉及后边复杂的渲染,由于项目保密性原因,不能贴全部代码,如果有需要可以评论~~ (下一篇会翻译一个各个api,英文文档看的好头大)
此致 敬礼!