canvas库fabric.js踩坑

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
禁止转载,如需转载请通过简信或评论联系作者。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容