JS前端輕量fabric.js系列物體基類
前言
在上個(gè)章節(jié)中我們已經(jīng)創(chuàng)建了畫布,接下來就可以進(jìn)行物體的繪制了,那具體要怎么畫呢?根據(jù)文章標(biāo)題可以猜到應(yīng)該是要抽象出一個(gè)物體基類,歸納出一些它們的共性,那它們能有啥共性呢,畢竟每個(gè)物體好像都是各畫各的。對(duì)于這個(gè)問題大家可以先簡(jiǎn)單思考幾秒鐘再往下看??。。。
FabricObject 基類的實(shí)現(xiàn)
抽離共同屬性
我們要繪制某個(gè)物體,那不就是在畫布的某個(gè)位置(top、left值)根據(jù)某些屬性(寬高大小等)畫上某個(gè)物體(比如矩形、多邊形、圖片或者路徑等等)嗎,并且之后還可以對(duì)每個(gè)物體進(jìn)行一些交互操作(主要就是平移+旋轉(zhuǎn)+縮放)。這么一說,是不是好像已經(jīng)把物體的挺多共性給抽離出來呢(真的是萬物皆對(duì)象啊,前端同學(xué)在 canvas 中尤其能體會(huì)到這個(gè)思想)。
那么,自然而然的我們就需要抽象出一個(gè)物體基類(FabricObject),其它物體(如 Rect)只需要繼承這個(gè)物體基類,就能夠很方便的擁有一些通用能力,對(duì)于日后的維護(hù)和擴(kuò)展也都是很友好的,看下面的代碼理解起來應(yīng)該會(huì)更清晰????:
class FabricObject { /** 物體類型標(biāo)識(shí) */ public type: string = 'object'; /** 是否可見 */ public visible: boolean = true; /** 是否處于激活態(tài),也就是是否被選中 */ public active: boolean = false; /** 物體位置的 top 值,就是 y */ public top: number = 0; /** 物體位置的 left 值,就是 x */ public left: number = 0; /** 物體的原始寬度 */ public width: number = 0; /** 物體的原始高度 */ public height: number = 0; /** 物體當(dāng)前的縮放倍數(shù) x */ public scaleX: number = 1; /** 物體當(dāng)前的縮放倍數(shù) y */ public scaleY: number = 1; /** 物體當(dāng)前的旋轉(zhuǎn)角度 */ public angle: number = 0; /** 默認(rèn)水平變換中心 left | right | center */ public originX: string = 'center'; /** 默認(rèn)垂直變換中心 top | bottom | center */ public originY: string = 'center'; /** 列舉常用的屬性 */ public stateProperties: string[] = ('top left width height scaleX scaleY ' + 'angle fill originX originY ' + 'stroke strokeWidth ' + 'borderWidth visible').split(' '); ... constructor(options) { this.initialize(options); // 初始化各種屬性,就是簡(jiǎn)單的賦值 } initialize(options) { options && this.setOptions(options); } render() {} // 繪制物體的方法 ... }
上面代碼中有幾個(gè)比較容易混淆的點(diǎn),就是 originX、originY 和 top、left,以及為啥不用 x、y 來表示物體位置呢?
解答之前,我們先來思考一個(gè)問題,如果要在畫布的 (x, y) 處繪制一個(gè) 100*100
的矩形,這句話會(huì)有什么歧義嗎?em。。。有的,看下下面這張圖????:
你會(huì)發(fā)現(xiàn)兩種畫法好像都沒錯(cuò),也都挺符合直覺,主要就是因?yàn)樗鼈兯x的中心點(diǎn)不一樣,所以就有了 originX 和 originY。
- 如果 originX = 'left', originY = 'top' 就是左圖那樣;
- 如果 originX = 'center', originY = 'center' 就是右圖那樣;
- 如果 originX = 'left', originY = 'bottom',那矩形就會(huì)畫在點(diǎn)(x, y) 的右上方;
- 以此類推... 新版本的 fabric.js 默認(rèn)采用的是左圖的方式,很早很早前是右圖的方式,當(dāng)然你可以自己傳參設(shè)置,靈活性杠杠滴。然后,現(xiàn)在你是不是會(huì)覺得 top、left 相比于 x、y 來說會(huì)稍微語義化點(diǎn)??。建議這幾個(gè)變量要好好理清一下,后續(xù)都是在此基礎(chǔ)上展開的。這里我覺得還是用 center 會(huì)直觀點(diǎn),所以這個(gè)系列采用的是右圖的方式,請(qǐng)務(wù)必記住。
抽離共同方法
物體最重要的一個(gè)方法就是 render 了,但是每個(gè)物體有各自獨(dú)特的繪制方法,能抽象出什么呢?想想好像沒啥能抽的。確實(shí)是這樣,所以我們嘗試先直接繪制幾個(gè)普通物體,再通過它們看看能不能倒推出一些通用的東西。
假設(shè)要在 (100, 100) 的地方繪制一個(gè) 50*50
的矩形,并將其放大 2 倍,之后旋轉(zhuǎn) 45°,該怎么畫呢?正常來說我們需要簡(jiǎn)單計(jì)算一下,就像這樣:
- 手動(dòng)算下寬高
100*100
- 手動(dòng)算下旋轉(zhuǎn)之后各個(gè)頂點(diǎn)的坐標(biāo)
- 連接四個(gè)頂點(diǎn) 如果是在畫布左下角畫一個(gè)邊長(zhǎng)為 100 的、擺的比較正的等邊三角形呢,就像這樣△?那我們也需要簡(jiǎn)單計(jì)算下:
- 手動(dòng)算下三角形每個(gè)頂點(diǎn)的坐標(biāo)
- 連接三個(gè)頂點(diǎn)
- 如果加上旋轉(zhuǎn),這個(gè)計(jì)算就更復(fù)雜了一些 又或者簡(jiǎn)單點(diǎn),我們?cè)?(100, 100) 處畫個(gè)圓,然后將其旋轉(zhuǎn) 30°,并把半徑縮小 2 倍,那就要:
- 因?yàn)槭莻€(gè)圓,所以不用考慮旋轉(zhuǎn),但是要算一下縮小后的半徑
- 畫一個(gè) (0, 2 * Math.PI) 的圓弧 所以上面三個(gè)小例子的共性就是:先計(jì)算再繪制嗎?不,不是的,我們?cè)?canvas 中要改掉這種繪制的思想,而是要通過并善用變換坐標(biāo)系來繪制物體,這個(gè)在上個(gè)章節(jié)末尾有提到,之所以這樣做,是因?yàn)樗軌蚬?jié)省很多計(jì)算和繪制成本。提到變換坐標(biāo)系,這個(gè)東西很容易讓人蒙圈,但它絕對(duì)是一把利器,所以我們必須要搞定它,如果你不熟悉,還是希望能夠多動(dòng)手練練,這樣才能拿捏它。
- 那現(xiàn)在我們應(yīng)該怎么畫呢?就是能用變換就用變換,能不計(jì)算就不計(jì)算。來看看上面第一個(gè)畫矩形的例子,首先我們繪制矩形的方法是固定的
ctx.fillRect(-width/2, -height/2, width, height);
,其中 width=50,height=50,然后就盡量不去動(dòng)它。那怎么畫出縮放和旋轉(zhuǎn)的效果,并且畫在點(diǎn) (100, 100) 的地方呢?就是用到之前說的變換坐標(biāo)系,簡(jiǎn)單看下代碼:
ctx.save(); // 之前提到過了,你要修改 ctx 上的一些配置或者畫一個(gè)物體,最好先 save 一下,這是個(gè)好習(xí)慣 ctx.translate(100, 100); // 此時(shí)原點(diǎn)已經(jīng)變到了 (100, 100) 的地方 ctx.scale(2, 2); // 坐標(biāo)系放大兩倍 ctx.rotate(Util.degreesToRadians(45)); // 注意 canvas 中用的都是弧度(弧度 / 2 * Math.PI = 角度 / 360),所以需要簡(jiǎn)單換算下 ctx.fillRect(-width/2, height/2, width, height); // 繪制矩形的方法固定不變,寬高一般也不會(huì)去修改 ctx.restore(); // 畫完之后還原 ctx 狀態(tài),這是個(gè)好習(xí)慣
再來看看第二個(gè)例子,在左下角畫一個(gè)邊長(zhǎng)為 100 的等邊三角形△,我們要做的就是先把原點(diǎn)移到三角形的某個(gè)頂點(diǎn)上(這里我們當(dāng)然拿左下角的頂點(diǎn)啦),然后通過不斷旋轉(zhuǎn)坐標(biāo)系繪制三條邊,看下代碼????:
ctx.save(); ctx.translate(0, 畫布高度); // 左下角變?yōu)?0, 0) 點(diǎn)了 ctx.rotate(Util.degreesToRadians(30)); // 準(zhǔn)備畫左邊這條邊 ctx.moveTo(0, 0); ctx.lineTo(100, 0); ctx.rotate(Util.degreesToRadians(120)); // 準(zhǔn)備畫右邊這條邊 ctx.lineTo(100, 0); ctx.rotate(Util.degreesToRadians(120)); // 準(zhǔn)備畫下面這條邊 ctx.lineTo(100, 0); ctx.restore();
大家可以在此基礎(chǔ)上畫一畫正多邊形,就能夠體會(huì)到旋轉(zhuǎn)的意思了。 至于第三個(gè)畫圓的例子,這里也簡(jiǎn)單放下代碼:
ctx.save(); ctx.translate(100, 100); ctx.scale(2, 2); ctx.arc(0, 0, r, 0, 2 * Math.PI); // 畫圓的方法始終不變 ctx.fill(); ctx.restore();
我們不再把物體上面的變換用于物體自身,而是用于坐標(biāo)系,從而簡(jiǎn)化了計(jì)算量和繪圖操作。
但可能還是不好看出來能抽象出什么(其實(shí)就只抽出了變換??),所以讓我們來看看代碼吧????:
class FabricObject { /** 渲染物體的通用流程 */ render(ctx: CanvasRenderingContext2D) { // 看不見的物體不繪制 if (this.width === 0 || this.height === 0 || !this.visible) return; // 凡是要變換坐標(biāo)系或者設(shè)置畫筆屬性都需要用先用 save 保存和再用 restore 還原,避免影響到其他東西的繪制 ctx.save(); // 1、坐標(biāo)變換 this.transform(ctx); // 2、繪制物體 this._render(ctx); ctx.restore(); } transform(ctx: CanvasRenderingContext2D) { ctx.translate(this.left, this.top); ctx.rotate(Util.degreesToRadians(this.angle)); ctx.scale(this.scaleX, this.scaleY); } /** 具體由子類來實(shí)現(xiàn),因?yàn)檫@確實(shí)是每個(gè)子類物體所獨(dú)有的 */ _render(ctx: CanvasRenderingContext2D) {} }
從上面的代碼中可以看到物體的繪制被分成了兩步:transform
和 _render
。
對(duì)于 transform
建議大家可以拿正多邊形和折線來找找感覺,本質(zhì)就是 n 條線段通過 translate 來不斷改變線段起始位置,通過 rotate 改變方向,通過 scale 來改變線段長(zhǎng)度,而繪制期間線段自身的長(zhǎng)度其實(shí)并沒有改變,然后畫之前在腦海里想一下每一條線段的效果,看看畫的是否與想的一致。記住核心思路(重要的事情說三遍??):
- 我們盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。
- 我們盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。
- 我們盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。 另外關(guān)于
transform
還要注意的是: - 變換是會(huì)疊加的,比如我 ctx.scale(2) 了之后又 ctx.scale(2),那最終的結(jié)果就是 ctx.scale(4),所以你還需要學(xué)會(huì)如何變換回去。一般有兩種方法:一種是配合 save 和 restore 使用,另一種就是往反方向進(jìn)行變換。
- 變換是有順序的,不同的順序最終繪制出來的效果也大不一樣,通常是 translate > rotate > scale,比較符合人的直覺。當(dāng)然你要用其他順序也是可以的,那重點(diǎn)是什么呢?重點(diǎn)是同一個(gè)庫或者引擎的內(nèi)部實(shí)現(xiàn)用的是同一種順序就行。
- 矩陣:其實(shí)這三種變換和矩陣是可以相互轉(zhuǎn)換的,就是把
transform
里面的函數(shù)換個(gè)寫法而已,我們用矩陣的形式matrix(a, b, c, d, tx, ty)
也能達(dá)到同樣的效果,但是矩陣更加強(qiáng)大并統(tǒng)一了寫法,而且除了三種基本的變換,還能達(dá)到其他效果,比如斜切 skew。關(guān)于矩陣的概念和寫法我們會(huì)在這個(gè)系列的最后幾個(gè)章節(jié)單獨(dú)講一下,目前我們可以暫且認(rèn)為這三種變換和矩陣是等價(jià)的。
scale 是沿著坐標(biāo)軸放大,并不一定是水平或豎直方向,假如物體旋轉(zhuǎn)了,就是沿著旋轉(zhuǎn)之后的坐標(biāo)軸方向放大,如下圖所示:
說完了 transform
,我們?cè)賮砜纯?_render
,這個(gè)就真沒啥共性了,需要由子類自己實(shí)現(xiàn)。
Rect 類的實(shí)現(xiàn)
接下來就趁熱打鐵,我們以一個(gè)最簡(jiǎn)單也最常用的 Rect 矩形類為例子來看看子類又是怎么操作的,這里直接上代碼,因?yàn)榇_實(shí)簡(jiǎn)單????:
/** 矩形類 */ class Rect extends FabricObject { /** 矩形標(biāo)識(shí) */ public type: string = 'rect'; /** 圓角 rx */ public rx: number = 0; /** 圓角 ry */ public ry: number = 0; constructor(options) { super(options); this._initStateProperties(); this._initRxRy(options); } /** 一些共有的和獨(dú)有的屬性 */ _initStateProperties() { this.stateProperties = this.stateProperties.concat(['rx', 'ry']); } /** 初始化圓角值 */ _initRxRy(options) { this.rx = options.rx || 0; this.ry = options.ry || 0; } /** 單純的繪制一個(gè)普普通通的矩形 */ _render(ctx: CanvasRenderingContext2D) { let rx = this.rx || 0, ry = this.ry || 0, x = -this.width / 2, y = -this.height / 2, w = this.width, h = this.height; // 繪制一個(gè)新的東西,大部分情況下都要開啟一個(gè)新路徑,要養(yǎng)成習(xí)慣 ctx.beginPath(); // 從左上角開始向右順時(shí)針畫一個(gè)矩形,這里就是單純的繪制一個(gè)規(guī)規(guī)矩矩的矩形 // 不考慮旋轉(zhuǎn)縮放啥的,因?yàn)樾D(zhuǎn)縮放會(huì)在調(diào)用 _render 函數(shù)之前處理 // 另外這里考慮了圓角的實(shí)現(xiàn),所以用到了貝塞爾曲線,不然你可以直接畫成四條線段,再懶一點(diǎn)可以直接調(diào)用原生方法 fillRect 和 strokeRect // 不過自己寫的話自由度更高,也方便擴(kuò)展 ctx.moveTo(x + rx, y); ctx.lineTo(x + w - rx, y); ctx.bezierCurveTo(x + w, y, x + w, y + ry, x + w, y + ry); ctx.lineTo(x + w, y + h - ry); ctx.bezierCurveTo(x + w, y + h, x + w - rx, y + h, x + w - rx, y + h); ctx.lineTo(x + rx, y + h); ctx.bezierCurveTo(x, y + h, x, y + h - ry, x, y + h - ry); ctx.lineTo(x, y + ry); ctx.bezierCurveTo(x, y, x + rx, y, x + rx, y); ctx.closePath(); if (this.fill) ctx.fill(); if (this.stroke) ctx.stroke(); } }
現(xiàn)在我們已經(jīng)有了一個(gè)最基礎(chǔ)也最為重要的一個(gè)物體:矩形。于是就可以將它添加到畫布中,我們?cè)谏弦徽鹿?jié)的 Canvas 類中加一個(gè) add 方法,如下代碼所示????:
class Canvas { /** * 添加元素 * 目前的模式是調(diào)用 add 添加物體的時(shí)候就立馬渲染,如果一次性加入大量元素,就會(huì)做很多無用功 * 所以可以優(yōu)化一下,就是先批量添加元素(需要加一個(gè)變量標(biāo)識(shí)),最后再統(tǒng)一渲染(手動(dòng)調(diào)用 renderAll 函數(shù)即可),這里先了解即可 */ add(...args): Canvas { this._objects.push(...args); this.renderAll(); return this; } /** 在下層畫布上繪制所有物體 */ renderAll(): Canvas { // 獲取下層畫布 const ctx = this.contextContainer; // 清除畫布 this.clearContext(ctx); // 簡(jiǎn)單粗暴的遍歷渲染 this._objects.forEach(object => { // render = transfrom + _render object.render(ctx); }) return this; } }
現(xiàn)在我們只需要傳入不同的參數(shù)就能在畫布中創(chuàng)建形形色色的矩形了,而子類里面的 _render
方法一般寫好了就行,很少會(huì)去動(dòng)它。
大家可以類比一下瀏覽器的盒模型,其實(shí)就是四四方方的矩形,然后用 css 中的 transfrom 做各種變換,也能達(dá)到各種效果,而元素的寬高大小并沒與改變。如果不理解為什么要拆成 transform 和 _render
兩部分,大家可以先記住,后面會(huì)體會(huì)到它的好。
當(dāng)然你可以能還有其他疑問,比如我們就直接遍歷所有物體嘛,繪制的物體一多這樣寫不會(huì)有問題嗎?關(guān)于這類問題我會(huì)在后面的性能優(yōu)化章節(jié)中講到,敬請(qǐng)期待,哈哈??
本章小結(jié)
這里就本章的內(nèi)容進(jìn)行一些小的總結(jié),這個(gè)章節(jié)我們主要學(xué)習(xí)了如何寫一個(gè)物體基類 FabricObject 以及最簡(jiǎn)單的子類實(shí)現(xiàn) Rect,一般物體的繪制大體可分為兩步:
- 1、先變換坐標(biāo)系(這個(gè)很重要,繪制物體、邊框、控制點(diǎn)都是要考慮變換坐標(biāo)系這個(gè)因素的)
- 2、單純的繪制圖形(比如矩形,就是在原點(diǎn)繪制一個(gè)規(guī)規(guī)矩矩的、沒有旋轉(zhuǎn)、沒有縮放的矩形) 更為重要的是我們應(yīng)該盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。另外還記得我們之前說過的畫布主要分為兩層,上層用來交互,下層用來繪制,現(xiàn)在已經(jīng)有了畫布類和物體類,下層畫布也就搞定了,接下來就可以搞搞上層交互了,那時(shí)大家就能體會(huì)到這樣繪制物體的好處了。
這里是簡(jiǎn)版 fabric.js 的代碼鏈接,有興趣的可以看看,也可以動(dòng)手去嘗試擴(kuò)展一些子類。 好啦,今天的分享就到這里,有什么問題歡迎點(diǎn)贊評(píng)論留言,下期再見,拜拜 ?? ??
實(shí)現(xiàn)一個(gè)輕量 fabric.js 系列二(畫布初始化)??
實(shí)現(xiàn)一個(gè)輕量 fabric.js 系列一(摸透 canvas)??
以上就是JS前端輕量fabric.js系列物體基類的詳細(xì)內(nèi)容,更多關(guān)于前端fabric.js物體基類的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Svelte調(diào)試模式j(luò)s級(jí)別差異和細(xì)化后的體積差異詳解
這篇文章主要為大家介紹了Svelte調(diào)試模式j(luò)s級(jí)別差異和細(xì)化后的體積差異詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12沒有resolve及reject的Promise是否會(huì)造成內(nèi)存泄露
這篇文章主要為大家介紹了一直沒有resolve及reject的Promise是否會(huì)造成內(nèi)存泄露的問題解決,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08微信小程序-getUserInfo回調(diào)的實(shí)例詳解
這篇文章主要介紹了微信小程序-getUserInfo回調(diào)的實(shí)例詳解的相關(guān)資料,希望通過本文能幫助到大家,讓大家理解掌握這部分內(nèi)容,需要的朋友可以參考下2017-10-10國(guó)慶節(jié)到了,利用JS實(shí)現(xiàn)一個(gè)生成國(guó)慶風(fēng)頭像的小工具 詳解實(shí)現(xiàn)過程
明天就是國(guó)慶節(jié)了,最近看到好多好友換了國(guó)慶風(fēng)的頭像,感覺這個(gè)挺有意思,就找到了類似的源碼研究了一番,并進(jìn)行了改造(并非原創(chuàng),只是進(jìn)行了改造,只要想分享一下實(shí)現(xiàn)思路)。下面就來看看如何實(shí)現(xiàn)一鍵生成國(guó)慶風(fēng)頭像小工具。​2021-09-09詳解Anyscript開發(fā)指南繞過typescript類型檢查
這篇文章主要為大家介紹了詳解Anyscript開發(fā)指南繞過typescript類型檢查,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-09-09