canvas庫fabric.js踩坑

fabric.js簡介

眾所周知渺鹦,canvas的api繁雜约巷,對一般的前端er來說不太友好枝冀,加上平時一般也不會自己手寫canvas痊夭,所以一般開發(fā)者對canvas的涉獵可能并不太深(我看紅寶書的時候canvas是直接跳過的)刁岸。而當(dāng)需要使用canvas開發(fā)一些定制化的需求時,echarts她我,antv系列虹曙,可能就無法滿足了,這個時候或許fabric會是一個比較好的選擇番舆,fabric提供一種類似面向?qū)ο蟮姆椒▉砭帉慶anvas酝碳,比原生稍微方便一些(然鵝官方文檔太難看懂了)

故事背景

近期的一個項目中,有這么一個需求:拖拽縮放元素并且進(jìn)行連線合蔽,本來我第一反應(yīng)是用antv/g6去實現(xiàn)的击敌,但是需要對拖拽的元素縮放并且拖拽的容器需要放文字和圖表介返,如果使用g6的話拴事,縮放容器,里面的內(nèi)容改變不太利索(實際是我對g6不太熟)圣蝎,另一個重要的問題是g6元素里面放圖表的話只能放g2(而且需要單獨安裝插件)并且不支持諸如tooltip等等功能刃宵,簡單來說只能用個閹割版的(示例:https://antv-g6.gitee.io/zh/examples/item/customNode#lineChartNode)。因此我最初想的是使用vue-grid-layout(github&&文檔)進(jìn)行拖拽與縮放徘公,畫線使用canvas牲证。這樣做的好處是第三方組件已經(jīng)把拖拽和縮放功能全都封裝好了,dom元素嵌入echarts和文本縮放也相當(dāng)方便(vue-echarts的autoresize关面,文本使用flex布局加overflow:auto)坦袍,當(dāng)然畫線又是一個大問題,關(guān)鍵點就是線要和拖拽的元素接上等太,簡而言之就是坐標(biāo)計算了捂齐。考慮到畫布里面還要放圖(拖拽的元素連線到圖上)以及要實現(xiàn)連線的時候鼠標(biāo)移動需要不停的重繪線缩抡,最終在同事的推薦下決定使用fabric.js來實現(xiàn)canvas部分奠宜。然后就發(fā)現(xiàn)這東西用起來一言難盡...

踩坑記錄與解決

1.官方文檔
就算你英語很好看他的文檔也會很別扭的,建議直接看官方DEMO找自己要的瞻想,不懂的百度谷歌压真,最后把查找文檔作為補(bǔ)充以及檢查是否有新版api和網(wǎng)上的古早文章不同。

2.在vue中使用

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

目前只能這樣用

3.繪制本地圖片有問題
我嘗試過fabric.Image.fromURL('xxx/xxx.png',function(){})以及new Image().src這兩種發(fā)現(xiàn)貌似都不能放本地圖片地址(類似@/assets/...這種)蘑险,可能是我使用的方式不對滴肿,最后只剩下一種方法可用了:

const imgDOM = document.getElementById('xxx');
imgDOM.onload = () => {
   const imgInstanceFirst = new Fabric.fabric.Image(imgDOM, {});
   this.fabricObj.add(imgInstanceFirst);
    // 將圖片層級降為最低
   imgInstanceFirst.sendToBack();
};

這種方法首先需要在頁面上放一個隱藏的img元素,結(jié)果一開始fabric還讀不到只能通過onload事件來獲取佃迄,但這樣會導(dǎo)致畫布重繪時無法執(zhí)行onload泼差,最后一個繪制圖片被我寫成這樣了

        // 繪制人體背景圖
        drawBodyImg() {
            const imgInstance = this.getBodyImgInstance();
            if (imgInstance) {
                this.fabricObj.add(imgInstance);
                imgInstance.sendToBack();
                return;
            }
            const imgDOM = document.getElementById('bodyImg');
            // 初始化時即使是已經(jīng)存在于html中的imgdom對象也需要在onload事件中獲取竿音,否則fabric渲染不出來
            imgDOM.onload = () => {
                // FIXME:某些未知情況暫時無法判斷 妥協(xié)做法初始化時渲染兩次并移除第一次渲染的圖
                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更新---------

可能是頁面結(jié)構(gòu)太復(fù)雜的緣故春瞬,上面的方法有小概率執(zhí)行時圖片還沒加載好,導(dǎo)致最后畫布里面其它內(nèi)容都出來了結(jié)果最重要的圖沒了套啤,最終我搞出來的解決辦法是宽气,在img標(biāo)簽上直接綁定load事件,執(zhí)行l(wèi)oad時將組件內(nèi)設(shè)置的狀態(tài)修改潜沦,并監(jiān)聽這個狀態(tài)的變化來執(zhí)行圖片渲染到canvas畫布的過程萄涯。

        <img
          v-show="false"
          id="bodyImg"
          src="@/assets/img/body.png"
          alt=""
          @load="loadBodyImg"
        />
        // .......
    watch: {
        // 圖片加載有時會比fabric加載慢
        bodyImgLoaded() {
            if (this.fabricObj) {
                // 避免重復(fù)加載
                const imgarr = this.fabricObj.getObjects().filter(v => {
                    return v._element && v.nodeName === 'IMG'; // 從控制臺打印獲取到fabricObj圖片內(nèi)部屬性
                })
                if (!imgarr.length) {
                    this.drawBodyImg();
                }
            }
        }
    }
// .....
        loadBodyImg() {
            this.bodyImgLoaded = true;
        },
        // 正常加載時還是先執(zhí)行這個方法,兩邊都有判斷唆鸡,不會重復(fù)執(zhí)行涝影,而且必定有一邊會執(zhí)行
        drawBodyImg() {
            const imgInstance = this.getBodyImgInstance();
            if (imgInstance) {
                this.fabricObj.add(imgInstance);
                imgInstance.sendToBack();
            }
        },

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

  1. 去除canvas對象的選中樣式以及功能
    fabric會默認(rèn)給每一個繪制出來的canvas對象加上縮放,旋轉(zhuǎn)等功能争占,你會看到畫布上的對象有一堆的點燃逻。我是這樣做的
    初始化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, // 關(guān)閉圖層控件
                hoverCursor: 'default'
            });

因為我需要點擊線的時候彈出刪除菜單臂痕,所以不能在初始化的時候直接skipTargetFind: true伯襟,我要做的是去除選中的樣式和大部分功能,保留選中時能獲取到選中對象握童,一旦這個屬性設(shè)為true則會取消所有選中樣式和功能姆怪,不需要在canvas對象里面再單獨配置了。

  1. 繪制三次貝塞爾曲線
    領(lǐng)導(dǎo)認(rèn)為直線不好看澡绩,UI直接整了一個三次貝塞爾曲線稽揭,所以有兩個問題,第一是如何在fabric里面繪制貝塞爾曲線肥卡,主要是用Path方法(應(yīng)該就是svg的畫法溪掀,注意M和C要大寫),(x1,y1) (x2, y2)分別是起點和終點召调,c1和c2是控制點坐標(biāo)(三次貝塞爾曲線需要兩個控制點)
/**
 * @description: 使用fabric繪制展示用的三次貝塞爾曲線
 * @param {Object} fabricObj 組件內(nèi)已經(jīng)生成的fabric對象
 * @param {Array<number>} start 起點坐標(biāo)
 * @param {Array<number>} end 終點坐標(biāo)
 * @param {String} strokeColor 線的顏色(展示用的默認(rèn)灰色)
 * @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 // 關(guān)閉圖層控件
    });
    fabricObj.add(line);
}

第二膨桥,計算三次貝塞爾曲線的控制點,這里面用了向量運算...

/**
 * @description: 已知起點和終點近似計算三次貝塞爾曲線控制點
 * @param {Array<number>} start 起點坐標(biāo)
 * @param {Array<number>} end 終點坐標(biāo)
 * @param {Number} curvature 曲率(默認(rèn)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. 最后碰到的一個很嚴(yán)重的問題唠叛,屏幕縮放問題
    fabric.js里面的坐標(biāo)系不能識別系統(tǒng)的縮放(是系統(tǒng)設(shè)置里面的縮放而非瀏覽器本身的縮放)只嚣,相信一般人windows電腦都會選擇系統(tǒng)推薦縮放吧,1080p甚至2k4k分辨率如果用原始比例的話字太小了艺沼,結(jié)果我把頁面從我的外接屏拖到筆記本的屏幕上時fabric里面的坐標(biāo)系直接崩壞了...

網(wǎng)上找的檢測屏幕縮放比例的方法(可以檢測到系統(tǒng)分辨率改變)

// 檢測屏幕縮放比例
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主要在拖動元素時計算元素與線的連接點坐標(biāo)用到了册舞,這個系統(tǒng)里面只要vue-grid-layout元素有改變,我就要重新計算線的起點并重繪線障般,通過這種辦法實現(xiàn)了dom元素和canvas元素的綁定调鲸,聽起來很low的樣子盛杰,不過最后功能是都實現(xiàn)了。

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

fabric視頻教程(我還沒看過藐石,可能有些內(nèi)容存在過時)
https://www.bilibili.com/video/BV1at411q7bt

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
禁止轉(zhuǎn)載即供,如需轉(zhuǎn)載請通過簡信或評論聯(lián)系作者。
  • 序言:七十年代末于微,一起剝皮案震驚了整個濱河市逗嫡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌株依,老刑警劉巖驱证,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異恋腕,居然都是意外死亡抹锄,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門荠藤,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伙单,“玉大人,你說我怎么就攤上這事商源〕捣荩” “怎么了谋减?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵牡彻,是天一觀的道長。 經(jīng)常有香客問我出爹,道長庄吼,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任严就,我火速辦了婚禮总寻,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘梢为。我一直安慰自己渐行,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布铸董。 她就那樣靜靜地躺著祟印,像睡著了一般。 火紅的嫁衣襯著肌膚如雪粟害。 梳的紋絲不亂的頭發(fā)上蕴忆,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機(jī)與錄音悲幅,去河邊找鬼套鹅。 笑死站蝠,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的卓鹿。 我是一名探鬼主播菱魔,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼吟孙!你這毒婦竟也來了豌习?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤拔疚,失蹤者是張志新(化名)和其女友劉穎肥隆,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體稚失,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡栋艳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了句各。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片吸占。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖凿宾,靈堂內(nèi)的尸體忽然破棺而出矾屯,到底是詐尸還是另有隱情,我是刑警寧澤初厚,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布件蚕,位于F島的核電站,受9級特大地震影響产禾,放射性物質(zhì)發(fā)生泄漏排作。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一亚情、第九天 我趴在偏房一處隱蔽的房頂上張望妄痪。 院中可真熱鬧,春花似錦楞件、人聲如沸衫生。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罪针。三九已至,卻和暖如春栅迄,著一層夾襖步出監(jiān)牢的瞬間站故,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留西篓,地道東北人愈腾。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像岂津,于是被迫代替她去往敵國和親虱黄。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容