您好,登錄后才能下訂單哦!
這篇文章主要介紹“JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)”,在日常操作中,相信很多人在JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
我們要繪制某個(gè)物體,那不就是在畫布的某個(gè)位置(top、left值)根據(jù)某些屬性(寬高大小等)畫上某個(gè)物體(比如矩形、多邊形、圖片或者路徑等等)嗎,并且之后還可以對每個(gè)物體進(jìn)行一些交互操作(主要就是平移+旋轉(zhuǎn)+縮放)。這么一說,是不是好像已經(jīng)把物體的挺多共性給抽離出來呢(真的是萬物皆對象啊,前端同學(xué)在 canvas 中尤其能體會到這個(gè)思想)。
那么,自然而然的我們就需要抽象出一個(gè)物體基類(FabricObject),其它物體(如 Rect)只需要繼承這個(gè)物體基類,就能夠很方便的擁有一些通用能力,對于日后的維護(hù)和擴(kuò)展也都是很友好的,看下面的代碼理解起來應(yīng)該會更清晰????????:
class FabricObject { /** 物體類型標(biāo)識 */ 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); // 初始化各種屬性,就是簡單的賦值 } initialize(options) { options && this.setOptions(options); } render() {} // 繪制物體的方法 ... }
上面代碼中有幾個(gè)比較容易混淆的點(diǎn),就是 originX、originY 和 top、left,以及為啥不用 x、y 來表示物體位置呢?
解答之前,我們先來思考一個(gè)問題,如果要在畫布的 (x, y) 處繪制一個(gè) 100*100
的矩形,這句話會有什么歧義嗎?em。。。有的,看下下面這張圖????????:
你會發(fā)現(xiàn)兩種畫法好像都沒錯(cuò),也都挺符合直覺,主要就是因?yàn)樗鼈兯x的中心點(diǎn)不一樣,所以就有了 originX 和 originY。
如果 originX = 'left', originY = 'top' 就是左圖那樣;
如果 originX = 'center', originY = 'center' 就是右圖那樣;
如果 originX = 'left', originY = 'bottom',那矩形就會畫在點(diǎn)(x, y) 的右上方;
以此類推... 新版本的 fabric.js 默認(rèn)采用的是左圖的方式,很早很早前是右圖的方式,當(dāng)然你可以自己傳參設(shè)置,靈活性杠杠滴。然后,現(xiàn)在你是不是會覺得 top、left 相比于 x、y 來說會稍微語義化點(diǎn)????。建議這幾個(gè)變量要好好理清一下,后續(xù)都是在此基礎(chǔ)上展開的。這里我覺得還是用 center 會直觀點(diǎn),所以這個(gè)系列采用的是右圖的方式,請務(wù)必記住。
物體最重要的一個(gè)方法就是 render 了,但是每個(gè)物體有各自獨(dú)特的繪制方法,能抽象出什么呢?想想好像沒啥能抽的。確實(shí)是這樣,所以我們嘗試先直接繪制幾個(gè)普通物體,再通過它們看看能不能倒推出一些通用的東西。
假設(shè)要在 (100, 100) 的地方繪制一個(gè) 50*50
的矩形,并將其放大 2 倍,之后旋轉(zhuǎn) 45°,該怎么畫呢?正常來說我們需要簡單計(jì)算一下,就像這樣:
手動(dòng)算下寬高 100*100
手動(dòng)算下旋轉(zhuǎn)之后各個(gè)頂點(diǎn)的坐標(biāo)
連接四個(gè)頂點(diǎn) 如果是在畫布左下角畫一個(gè)邊長為 100 的、擺的比較正的等邊三角形呢,就像這樣△?那我們也需要簡單計(jì)算下:
手動(dòng)算下三角形每個(gè)頂點(diǎn)的坐標(biāo)
連接三個(gè)頂點(diǎn)
如果加上旋轉(zhuǎn),這個(gè)計(jì)算就更復(fù)雜了一些 又或者簡單點(diǎn),我們在 (100, 100) 處畫個(gè)圓,然后將其旋轉(zhuǎn) 30°,并把半徑縮小 2 倍,那就要:
因?yàn)槭莻€(gè)圓,所以不用考慮旋轉(zhuǎn),但是要算一下縮小后的半徑
畫一個(gè) (0, 2 * Math.PI) 的圓弧 所以上面三個(gè)小例子的共性就是:先計(jì)算再繪制嗎?不,不是的,我們在 canvas 中要改掉這種繪制的思想,而是要通過并善用變換坐標(biāo)系來繪制物體,這個(gè)在上個(gè)章節(jié)末尾有提到,之所以這樣做,是因?yàn)樗軌蚬?jié)省很多計(jì)算和繪制成本。提到變換坐標(biāo)系,這個(gè)東西很容易讓人蒙圈,但它絕對是一把利器,所以我們必須要搞定它,如果你不熟悉,還是希望能夠多動(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)系,簡單看下代碼:
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),所以需要簡單換算下 ctx.fillRect(-width/2, height/2, width, height); // 繪制矩形的方法固定不變,寬高一般也不會去修改 ctx.restore(); // 畫完之后還原 ctx 狀態(tài),這是個(gè)好習(xí)慣
再來看看第二個(gè)例子,在左下角畫一個(gè)邊長為 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ǔ)上畫一畫正多邊形,就能夠體會到旋轉(zhuǎn)的意思了。 至于第三個(gè)畫圓的例子,這里也簡單放下代碼:
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)系,從而簡化了計(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
。
對于 transform
建議大家可以拿正多邊形和折線來找找感覺,本質(zhì)就是 n 條線段通過 translate 來不斷改變線段起始位置,通過 rotate 改變方向,通過 scale 來改變線段長度,而繪制期間線段自身的長度其實(shí)并沒有改變,然后畫之前在腦海里想一下每一條線段的效果,看看畫的是否與想的一致。記住核心思路(重要的事情說三遍????):
我們盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。
我們盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。
我們盡量不去改變物體的寬高和大小,而是通過各種變換來達(dá)到所需要的效果。 另外關(guān)于 transform
還要注意的是:
變換是會疊加的,比如我 ctx.scale(2) 了之后又 ctx.scale(2),那最終的結(jié)果就是 ctx.scale(4),所以你還需要學(xué)會如何變換回去。一般有兩種方法:一種是配合 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)于矩陣的概念和寫法我們會在這個(gè)系列的最后幾個(gè)章節(jié)單獨(dú)講一下,目前我們可以暫且認(rèn)為這三種變換和矩陣是等價(jià)的。
scale 是沿著坐標(biāo)軸放大,并不一定是水平或豎直方向,假如物體旋轉(zhuǎn)了,就是沿著旋轉(zhuǎn)之后的坐標(biāo)軸方向放大,如下圖所示:
說完了 transform
,我們再來看看 _render
,這個(gè)就真沒啥共性了,需要由子類自己實(shí)現(xiàn)。
接下來就趁熱打鐵,我們以一個(gè)最簡單也最常用的 Rect 矩形類為例子來看看子類又是怎么操作的,這里直接上代碼,因?yàn)榇_實(shí)簡單????????:
/** 矩形類 */ class Rect extends FabricObject { /** 矩形標(biāo)識 */ 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)縮放會在調(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è)物體:矩形。于是就可以將它添加到畫布中,我們在上一章節(jié)的 Canvas 類中加一個(gè) add 方法,如下代碼所示????????:
class Canvas { /** * 添加元素 * 目前的模式是調(diào)用 add 添加物體的時(shí)候就立馬渲染,如果一次性加入大量元素,就會做很多無用功 * 所以可以優(yōu)化一下,就是先批量添加元素(需要加一個(gè)變量標(biāo)識),最后再統(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); // 簡單粗暴的遍歷渲染 this._objects.forEach(object => { // render = transfrom + _render object.render(ctx); }) return this; } }
現(xiàn)在我們只需要傳入不同的參數(shù)就能在畫布中創(chuàng)建形形色色的矩形了,而子類里面的 _render
方法一般寫好了就行,很少會去動(dòng)它。
大家可以類比一下瀏覽器的盒模型,其實(shí)就是四四方方的矩形,然后用 css 中的 transfrom 做各種變換,也能達(dá)到各種效果,而元素的寬高大小并沒與改變。如果不理解為什么要拆成 transform 和 _render
兩部分,大家可以先記住,后面會體會到它的好。
到此,關(guān)于“JS前端輕量fabric.js系列物體基類怎么實(shí)現(xiàn)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。