使用cropper实现“在线批改”功能

864 阅读6分钟

前言

最近实现了一个“在线”批改的功能,其中包括截图,旋转,放大缩小,自适应等功能,调研了几个可支持截图的api,最终选择cropper来实现,以下介绍过程中总结的技巧以及避坑指南。

分以下几个阶段介绍

  • cropper的简单介绍
  • 项目背景 & 需要实现的主要技术点
  • 实现过程中难点方案的技术实现设计
  • 结语

cropper的简单介绍

调研了几个方向:原生canvas,fabric,cropper; 最后突出重围的即为cropper,cropper的主打点为截图,契合需求,git地址 || 官方demo 官方demo实现的比较可用了,直观的截图,旋转等都符合需求

react形式的基本使用

  1. yarn add react-cropper
  2. 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的引入形式

项目背景 & 需要实现的主要技术点

项目需要做线上模拟老师的批改操作,为学生的卷子上框出错题,并填写错因等,拆开来说 需要基本的截图功能,并且卷子依赖学生上传,可能方向不对,所以需要调整,需要放大,缩小,旋转,自适应, 并且一张卷子可能会有多个错题,所以需要记录老师之前框选的错题位置,方便下次回看,所以需要实现主要点为

  1. 基本的截图
  2. 截图位置回显
  3. 放大缩小
  4. 旋转
  5. 自适应

实现过程中难点方案的技术实现设计

基本截图

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,英文文档看的好头大)

此致 敬礼!