上篇文章大致介绍了使用Vue + fabric.js构建标注工具的流程,本篇则将其中的一些细节以及fabric
的踩坑进行补充
1.鼠标从右向左画框
承接上篇的描述,使用fabric
在canvas上画标注框的流程主要为:
- 监听画布的鼠标按下
mouse:down
事件,并保存鼠标按下时的坐标,作为标注框的起点(mouseFrom
); - 监听画布的鼠标移动
mouse:move
事件,在鼠标移动过程中,在canvas上绘制以第一步中的起点为左上角,鼠标移动时的坐标为右下角(mouseTo
)的矩形(rect); - 监听画布的鼠标抬起
mouse:up
事件,鼠标抬起时,标注框绘制完毕; 由此得知,在第二步中的标注框的生成代码为
rect = new fabric.Rect({
left: mouseFrom.x,
top: mouseFrom.y,
width: mouseTo.x - mouseFrom.x,
height: mouseTo.y - mouseFrom.y
})
然而这样设置存在一个隐患bug,当鼠标从左向右画框时,标注框正常,但当鼠标从右向左画框时,发现标注框并不能如我们所期望的随着鼠标移动,而是一直向右画框
针对上面场景,一个解决方案为
在绘制框时,先判断mouseFrom.x
和mouseTo.x
,mouseFrom.y
和mouseTo.y
的大小,以较小的那个值为标注框的左上角的坐标(left
和top
),以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是随着鼠标从右向左画
2.标注框溢出画布
- 绘制过程中标注框溢出画布
紧接着上步所说的跟随着鼠标移动绘制标注框,当鼠标在画布内的时候,标注框正常绘制,但是,当鼠标移出画布时,
mouseFrom
和mouseTo
的值仍在变化,但是溢出画布的标注框却不能正常显示,因此在绘制时,需要限制mouseFrom
和mouseTo
的值,使得标注框的起点和终点均保持在画布内部。
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()
}
- 移动标注框过程中溢出画布
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,起初在外接显示器上,选中标注框正常,但无意间拖动到自己电脑屏幕上时,诡异的一幕发生了,选中的框跟原本的标注框不对应,再拖回到外接显示器上,又显示正常了
选中状态下选中选中框的八个控制点没有很好的附着在选中框上
看到这个问题,着实让人头疼,明明什么都没动,为啥会出现这样的bug?逐一对比在外接显示器和自己电脑屏幕上console出来的被选中的标注框的各个字段,发现zoomX
和zoomY
在外接显示器上为1,在自己电脑屏幕上为1.25,不由怀疑是zoomX
和zoomY
这两个字段导致的标注框偏移,然后去研究源码,找到在创建标注框rect时zoomX
和zoomY
的赋值逻辑
fabric
是通过drawControls()
函数绘制选中状态下的控制点的,其中红线框的部分发现设置了transform
,紧接着怀疑是canvas的getRetinalScaling()
影响到了zoomX
和zoomY
找到
getRetinalScaling()
的取值函数,发现是根据_isRetinaScaling()
函数来决定取fabric.devicePixelRatio
还是默认值1,不理解fabric.devicePixelRatio
是什么,就接着去找fabric.devicePixelRatio
的定义
window.devicePixelRatio
到这,恍然大悟,检查自己电脑的分配率设置,果然是125%,与上面所述打印出来的rect的
zoomX
和zoomY
对应,试着将分辨率改成100%,发现zoomX
和zoomY
值变为1,选中状态下的控制点也显示正常了
理清bug出现的原因后,自然而然就想到,解决此bug的关键点在于不能让window.devicePixelRatio
成为控制点的缩放因子,问题又回到了getRetinalScaling()
,如果_isRetinaScaling()
为false,那不管屏幕分辨率是多少,getRetinalScaling()
值都取1,控制点不就显示正常了?
然后接着去找
_isRetinaScaling()
的取值
发现fabric的canvas有一个
enableRetinaScaling
参数,默认值为true,官网给出的参数含义为
单看文档,确实不知所云,但通过源码,很好的就理解了参数的含义,感叹一声,文档还是要配合源码观看效果更佳!
4.选中状态下调整框的等比例缩放问题
开发完之后,产品提出这样一个bug,调整标注框拖动上下左右四个角只能等比例缩放,产品期望能随着鼠标自由地缩放,浏览一遍文档,没有找到对应的设置,那就只能再去源码里面找了,寻找的过程在这里就不啰嗦了,总而言之,通过自下而上地翻阅源码,发现fabric的canvas有一个uniformScaling
属性控制着标注框的等比例缩放,且默认值为true,将其设置成false后,bug就迎刃而解了
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
...