美文网首页全栈笔记
canvas库fabric.js踩坑

canvas库fabric.js踩坑

作者: 韭菜的故事 | 来源:发表于2021-03-22 20:08 被阅读0次

    fabric.js简介

    众所周知,canvas的api繁杂,对一般的前端er来说不太友好,加上平时一般也不会自己手写canvas,所以一般开发者对canvas的涉猎可能并不太深(我看红宝书的时候canvas是直接跳过的)。而当需要使用canvas开发一些定制化的需求时,echarts,antv系列,可能就无法满足了,这个时候或许fabric会是一个比较好的选择,fabric提供一种类似面向对象的方法来编写canvas,比原生稍微方便一些(然鹅官方文档太难看懂了)

    故事背景

    近期的一个项目中,有这么一个需求:拖拽缩放元素并且进行连线,本来我第一反应是用antv/g6去实现的,但是需要对拖拽的元素缩放并且拖拽的容器需要放文字和图表,如果使用g6的话,缩放容器,里面的内容改变不太利索(实际是我对g6不太熟),另一个重要的问题是g6元素里面放图表的话只能放g2(而且需要单独安装插件)并且不支持诸如tooltip等等功能,简单来说只能用个阉割版的(示例:https://antv-g6.gitee.io/zh/examples/item/customNode#lineChartNode)。因此我最初想的是使用vue-grid-layout(github&&文档)进行拖拽与缩放,画线使用canvas。这样做的好处是第三方组件已经把拖拽和缩放功能全都封装好了,dom元素嵌入echarts和文本缩放也相当方便(vue-echarts的autoresize,文本使用flex布局加overflow:auto),当然画线又是一个大问题,关键点就是线要和拖拽的元素接上,简而言之就是坐标计算了。考虑到画布里面还要放图(拖拽的元素连线到图上)以及要实现连线的时候鼠标移动需要不停的重绘线,最终在同事的推荐下决定使用fabric.js来实现canvas部分。然后就发现这东西用起来一言难尽...

    踩坑记录与解决

    1.官方文档
    就算你英语很好看他的文档也会很别扭的,建议直接看官方DEMO找自己要的,不懂的百度谷歌,最后把查找文档作为补充以及检查是否有新版api和网上的古早文章不同。

    2.在vue中使用

    import Fabric from 'fabric';
    new Fabric.fabric.Canvas('xxx',{});
    

    目前只能这样用

    3.绘制本地图片有问题
    我尝试过fabric.Image.fromURL('xxx/xxx.png',function(){})以及new Image().src这两种发现貌似都不能放本地图片地址(类似@/assets/...这种),可能是我使用的方式不对,最后只剩下一种方法可用了:

    const imgDOM = document.getElementById('xxx');
    imgDOM.onload = () => {
       const imgInstanceFirst = new Fabric.fabric.Image(imgDOM, {});
       this.fabricObj.add(imgInstanceFirst);
        // 将图片层级降为最低
       imgInstanceFirst.sendToBack();
    };
    

    这种方法首先需要在页面上放一个隐藏的img元素,结果一开始fabric还读不到只能通过onload事件来获取,但这样会导致画布重绘时无法执行onload,最后一个绘制图片被我写成这样了

            // 绘制人体背景图
            drawBodyImg() {
                const imgInstance = this.getBodyImgInstance();
                if (imgInstance) {
                    this.fabricObj.add(imgInstance);
                    imgInstance.sendToBack();
                    return;
                }
                const imgDOM = document.getElementById('bodyImg');
                // 初始化时即使是已经存在于html中的imgdom对象也需要在onload事件中获取,否则fabric渲染不出来
                imgDOM.onload = () => {
                    // FIXME:某些未知情况暂时无法判断 妥协做法初始化时渲染两次并移除第一次渲染的图
                    this.fabricObj.remove(imgInstance);
                    const imgInstanceFirst = new Fabric.fabric.Image(imgDOM, {...});
                    this.fabricObj.add(imgInstanceFirst);
                    imgInstanceFirst.sendToBack();
                };
            },
            // 尝试获取人体图实例
            getBodyImgInstance() {
                const imgDOM = document.getElementById('bodyImg');
                const imgInstance = new Fabric.fabric.Image(imgDOM, {...});
                if (imgInstance.height) {
                    return imgInstance;
                } else {
                    return null;
                }
            },
    

    sendToBack方法是为了确保在后面画线的时候线能在图的上面一层显示(貌似fabric是按照先后绘制顺序排层级的,先绘制的层级最高,于是我们需要将图的层级降到最低)

    ------------- 2021.03.30更新---------

    可能是页面结构太复杂的缘故,上面的方法有小概率执行时图片还没加载好,导致最后画布里面其它内容都出来了结果最重要的图没了,最终我搞出来的解决办法是,在img标签上直接绑定load事件,执行load时将组件内设置的状态修改,并监听这个状态的变化来执行图片渲染到canvas画布的过程。

            <img
              v-show="false"
              id="bodyImg"
              src="@/assets/img/body.png"
              alt=""
              @load="loadBodyImg"
            />
            // .......
        watch: {
            // 图片加载有时会比fabric加载慢
            bodyImgLoaded() {
                if (this.fabricObj) {
                    // 避免重复加载
                    const imgarr = this.fabricObj.getObjects().filter(v => {
                        return v._element && v.nodeName === 'IMG'; // 从控制台打印获取到fabricObj图片内部属性
                    })
                    if (!imgarr.length) {
                        this.drawBodyImg();
                    }
                }
            }
        }
    // .....
            loadBodyImg() {
                this.bodyImgLoaded = true;
            },
            // 正常加载时还是先执行这个方法,两边都有判断,不会重复执行,而且必定有一边会执行
            drawBodyImg() {
                const imgInstance = this.getBodyImgInstance();
                if (imgInstance) {
                    this.fabricObj.add(imgInstance);
                    imgInstance.sendToBack();
                }
            },
    
    

    -----------------------------

    1. 去除canvas对象的选中样式以及功能
      fabric会默认给每一个绘制出来的canvas对象加上缩放,旋转等功能,你会看到画布上的对象有一堆的点。我是这样做的
      初始化fabric对象
                this.fabricObj = new Fabric.fabric.Canvas('canvasPart', {
                    selection: false, // 不可框选
                    skipTargetFind: false // 保留选中操作(在canvas对象中去掉选中样式)
                });
    

    画图(画线除了selectable其它类似,因为我的项目需要选中线)

            const imgInstance = new Fabric.fabric.Image(imgDOM, {
                    selectable: false, // 去掉选中的效果
                    hasControls: false, // 关闭图层控件
                    hoverCursor: 'default'
                });
    

    因为我需要点击线的时候弹出删除菜单,所以不能在初始化的时候直接skipTargetFind: true,我要做的是去除选中的样式和大部分功能,保留选中时能获取到选中对象,一旦这个属性设为true则会取消所有选中样式和功能,不需要在canvas对象里面再单独配置了。

    1. 绘制三次贝塞尔曲线
      领导认为直线不好看,UI直接整了一个三次贝塞尔曲线,所以有两个问题,第一是如何在fabric里面绘制贝塞尔曲线,主要是用Path方法(应该就是svg的画法,注意M和C要大写),(x1,y1) (x2, y2)分别是起点和终点,c1和c2是控制点坐标(三次贝塞尔曲线需要两个控制点)
    /**
     * @description: 使用fabric绘制展示用的三次贝塞尔曲线
     * @param {Object} fabricObj 组件内已经生成的fabric对象
     * @param {Array<number>} start 起点坐标
     * @param {Array<number>} end 终点坐标
     * @param {String} strokeColor 线的颜色(展示用的默认灰色)
     * @return {*}
     */
    export function drawCubicBezierCurve(fabricObj, start, end, strokeColor = '#768C8C') {
        const x1 = start[0];
        const y1 = start[1];
        const x2 = end[0];
        const y2 = end[1];
        const c1 = calcControlPoint(start, end).c1;
        const c2 = calcControlPoint(start, end).c2;
        const line = new Fabric.fabric.Path(`M ${x1} ${y1}C${c1[0]},${c1[1]},${c2[0]},${c2[1]},${x2},${y2}`, {
            stroke: strokeColor,
            hoverCursor: 'default',
            fill: false,
            hasControls: false // 关闭图层控件
        });
        fabricObj.add(line);
    }
    

    第二,计算三次贝塞尔曲线的控制点,这里面用了向量运算...

    /**
     * @description: 已知起点和终点近似计算三次贝塞尔曲线控制点
     * @param {Array<number>} start 起点坐标
     * @param {Array<number>} end 终点坐标
     * @param {Number} curvature 曲率(默认0.1)
     * @return {Object}
     */
    export function calcControlPoint(start, end, curvature = 0.1) {
        const x1 = start[0];
        const y1 = start[1];
        const x2 = end[0];
        const y2 = end[1];
        const cx1 = x1 + (x2 - x1) / 3 + (y2 - y1) * curvature;
        const cy1 = y1 + (y2 - y1) / 3 + (x1 - x2) * curvature;
        const cx2 = x1 + (x2 - x1) * 2 / 3 + (y1 - y2) * curvature;
        const cy2 = y1 + (y2 - y1) * 2 / 3 + (x2 - x1) * curvature;
        return {
            c1: [Math.abs(cx1), Math.abs(cy1)],
            c2: [Math.abs(cx2), Math.abs(cy2)]
        };
    }
    
    1. 最后碰到的一个很严重的问题,屏幕缩放问题
      fabric.js里面的坐标系不能识别系统的缩放(是系统设置里面的缩放而非浏览器本身的缩放),相信一般人windows电脑都会选择系统推荐缩放吧,1080p甚至2k4k分辨率如果用原始比例的话字太小了,结果我把页面从我的外接屏拖到笔记本的屏幕上时fabric里面的坐标系直接崩坏了...

    网上找的检测屏幕缩放比例的方法(可以检测到系统分辨率改变)

    // 检测屏幕缩放比例
    export function detectZoom() {
        let ratio = 0;
        const screen = window.screen;
        const ua = navigator.userAgent.toLowerCase();
        if (window.devicePixelRatio !== undefined) {
            ratio = window.devicePixelRatio;
        } else if (~ua.indexOf('msie')) {
            if (screen.deviceXDPI && screen.logicalXDPI) {
                ratio = screen.deviceXDPI / screen.logicalXDPI;
            }
        } else if (window.outerWidth !== undefined && window.innerWidth !== undefined) {
            ratio = window.outerWidth / window.innerWidth;
        }
        return ratio;
    }
    

    然后在初始化fabric对象时需要重新计算宽高(canvasLayout为画布上一级的父元素)

                this.fabricObj = new Fabric.fabric.Canvas('canvasPart', {
                    selection: false, // 不可框选
                    skipTargetFind: false // 保留选中操作(在Line中去掉选中样式)
                });
                const boxDOM = document.getElementById('canvasLayout');
                const width = boxDOM.offsetWidth / this.pageZoom;
                const height = boxDOM.offsetHeight / this.pageZoom;
                this.fabricObj.setWidth(width);
                this.fabricObj.setHeight(height);
                this.fabricObj.renderAll();
    

    pageZoom主要在拖动元素时计算元素与线的连接点坐标用到了,这个系统里面只要vue-grid-layout元素有改变,我就要重新计算线的起点并重绘线,通过这种办法实现了dom元素和canvas元素的绑定,听起来很low的样子,不过最后功能是都实现了。

    参考文章(还有些讲fabric的api的文章找不到了...)
    https://github.com/hujiulong/blog/issues/1

    fabric视频教程(我还没看过,可能有些内容存在过时)
    https://www.bilibili.com/video/BV1at411q7bt

    相关文章

      网友评论

        本文标题:canvas库fabric.js踩坑

        本文链接:https://www.haomeiwen.com/subject/fwhthltx.html