使用Vue + fabric.js构建标注工具的细节

3,505

上篇文章大致介绍了使用Vue + fabric.js构建标注工具的流程,本篇则将其中的一些细节以及fabric的踩坑进行补充

1.鼠标从右向左画框

承接上篇的描述,使用fabric在canvas上画标注框的流程主要为:

  1. 监听画布的鼠标按下mouse:down事件,并保存鼠标按下时的坐标,作为标注框的起点(mouseFrom);
  2. 监听画布的鼠标移动mouse:move事件,在鼠标移动过程中,在canvas上绘制以第一步中的起点为左上角,鼠标移动时的坐标为右下角(mouseTo)的矩形(rect);
  3. 监听画布的鼠标抬起mouse:up事件,鼠标抬起时,标注框绘制完毕; 由此得知,在第二步中的标注框的生成代码为
rect = new fabric.Rect({
    left: mouseFrom.x,
    top: mouseFrom.y,
    width: mouseTo.x - mouseFrom.x,
    height: mouseTo.y - mouseFrom.y
    })

然而这样设置存在一个隐患bug,当鼠标从左向右画框时,标注框正常,但当鼠标从右向左画框时,发现标注框并不能如我们所期望的随着鼠标移动,而是一直向右画框 291891492291361822021-09-02_16.17.20.gif 针对上面场景,一个解决方案为

在绘制框时,先判断mouseFrom.xmouseTo.x,mouseFrom.ymouseTo.y的大小,以较小的那个值为标注框的左上角的坐标(lefttop),以mouseTo.x-mouseFrom.x的绝对值为标注框的宽(width),以mouseTo.y-mouseFrom.y的绝对值为标注框的高(height)

let x = Math.min(mouseFrom.x, mouseTo.x)
let y = Math.min(mouseFrom.y, mouseTo.y)
let width = Math.abs(mouseTo.x-mouseFrom.x)
let height = Math.abs(mouseTo.y-mouseFrom.y)
rect = new fabric.Rect({
    left: x,
    top: y,
    width: width,
    height: height
    })

以这样的方法使得标注框的左上定点是相对小的那个值,虽然rect仍旧是从左画到右,但随着鼠标的移动,视觉上rect是随着鼠标从右向左画

291891492291361822021-09-02_16.44.57.gif

2.标注框溢出画布

  • 绘制过程中标注框溢出画布 紧接着上步所说的跟随着鼠标移动绘制标注框,当鼠标在画布内的时候,标注框正常绘制,但是,当鼠标移出画布时,mouseFrommouseTo的值仍在变化,但是溢出画布的标注框却不能正常显示,因此在绘制时,需要限制mouseFrommouseTo的值,使得标注框的起点和终点均保持在画布内部。
limitPoint(x,y){
    if(x < 0) x = 0
    if(y < 0) y = 0
    // fabricObj为使用fabric创建的canvas对象,this.fabricObj.getWidth()获取画布的宽
    if(x > this.fabricObj.getWidth()) x = this.fabricObj.getWidth()
    // this.fabricObj.getHeight()获取画布的高
    if(y > this.fabricObj.getHeight()) y = this.fabricObj.getHeight()
}

291891492291361822021-09-03_08.47.10.gif

  • 移动标注框过程中溢出画布

canvas.on('object:moving', (e) => {

// 阻止对象移动到画布外面
      let padding = 0; // 内容距离画布的空白宽度,主动设置
      var obj = e.target;
      if (obj.currentHeight > obj.canvas.height - padding * 2 ||
        obj.currentWidth > obj.canvas.width - padding * 2) {
        return;
      }
      obj.setCoords();
      if (obj.getBoundingRect().top < padding || obj.getBoundingRect().left < padding) {
        obj.top = Math.max(obj.top, obj.top - obj.getBoundingRect().top + padding);
        obj.left = Math.max(obj.left, obj.left - obj.getBoundingRect().left + padding);
      }
      if (obj.getBoundingRect().top + obj.getBoundingRect().height > obj.canvas.height - padding || obj.getBoundingRect().left + obj.getBoundingRect().width > obj.canvas.width - padding) {
        obj.top = Math.min(
          obj.top,
          obj.canvas.height - obj.getBoundingRect().height + obj.top - obj.getBoundingRect().top - padding
        );
        obj.left = Math.min(
          obj.left,
          obj.canvas.width - obj.getBoundingRect().width + obj.left - obj.getBoundingRect().left - padding
        );
      }

})

3.屏幕分辨率引起的选中状态下的框移位

在开发过程中,我遇到过这样一个bug,起初在外接显示器上,选中标注框正常,但无意间拖动到自己电脑屏幕上时,诡异的一幕发生了,选中的框跟原本的标注框不对应,再拖回到外接显示器上,又显示正常了

image.png

选中状态下选中选中框的八个控制点没有很好的附着在选中框上

看到这个问题,着实让人头疼,明明什么都没动,为啥会出现这样的bug?逐一对比在外接显示器和自己电脑屏幕上console出来的被选中的标注框的各个字段,发现zoomXzoomY在外接显示器上为1,在自己电脑屏幕上为1.25,不由怀疑是zoomXzoomY这两个字段导致的标注框偏移,然后去研究源码,找到在创建标注框rect时zoomXzoomY的赋值逻辑

fabric是通过drawControls()函数绘制选中状态下的控制点的,其中红线框的部分发现设置了transform,紧接着怀疑是canvas的getRetinalScaling()影响到了zoomXzoomY image.png image.png 找到getRetinalScaling()的取值函数,发现是根据_isRetinaScaling()函数来决定取fabric.devicePixelRatio还是默认值1,不理解fabric.devicePixelRatio是什么,就接着去找fabric.devicePixelRatio的定义 image.png window.devicePixelRatio image.png 到这,恍然大悟,检查自己电脑的分配率设置,果然是125%,与上面所述打印出来的rect的zoomXzoomY对应,试着将分辨率改成100%,发现zoomXzoomY值变为1,选中状态下的控制点也显示正常了

理清bug出现的原因后,自然而然就想到,解决此bug的关键点在于不能让window.devicePixelRatio成为控制点的缩放因子,问题又回到了getRetinalScaling(),如果_isRetinaScaling()为false,那不管屏幕分辨率是多少,getRetinalScaling()值都取1,控制点不就显示正常了? image.png 然后接着去找_isRetinaScaling()的取值 image.png 发现fabric的canvas有一个enableRetinaScaling参数,默认值为true,官网给出的参数含义为

image.png 单看文档,确实不知所云,但通过源码,很好的就理解了参数的含义,感叹一声,文档还是要配合源码观看效果更佳!

4.选中状态下调整框的等比例缩放问题

开发完之后,产品提出这样一个bug,调整标注框拖动上下左右四个角只能等比例缩放,产品期望能随着鼠标自由地缩放,浏览一遍文档,没有找到对应的设置,那就只能再去源码里面找了,寻找的过程在这里就不啰嗦了,总而言之,通过自下而上地翻阅源码,发现fabric的canvas有一个uniformScaling属性控制着标注框的等比例缩放,且默认值为true,将其设置成false后,bug就迎刃而解了

291891492291361822021-09-03_08.58.01.gif

5.图片分辨率不同,标注框的宽度设置

由于不同的图片分辨率差异较大,如果以同一种宽度来设置标注框,呈现效果相差较大,因此采取根据图片分辨率来动态设置标注框宽度(scale为上篇文章中创建画布阶段,图片宽高与画布容器宽高的比值)

 <div id="canvax-box">
        <canvas id="label-canvas" :width="width" :height="height">
    </div>
</template>
<script>
 export default{
     methods:{
         fabricCanvas(){
                 ...
                 // 将图片放置在外部容器中
                 let boxWidth = document.getElementById('canvas-box').offsetWidth
                 let boxHeight = document.getElementById('canvas-box').offsetHeight
                 let scaleX = boxWidth / image.width
                 let scaleY = boxHeight / image.height
                 // 确定缩放因子
                 this.scale = scaleX > scaleY ? scaleX : scaleY
                 ...

image.png

image.png