fabric.js圖層功能獨立顯隱?添加?刪除?預(yù)覽實現(xiàn)詳解
引言
去年經(jīng)歷了個虛擬人的項目,其中我參與了前端的部分,一個用electron寫的編輯器,UI部分用的vue3+tsx的寫法(這種組合是不是沒見過?我也是第一次見,驚得我當時還發(fā)了個沸點)

我所負責的部分是讓用戶可以對貼圖進行修改,其中就涉及到了圖層功能(類似Photoshop),而我當時選用的fabric是沒有圖層的,因此我就得考慮如何實現(xiàn)圖層
原理
fabric本身有提供group功能,本意是讓你將畫布上的一些元素組合起來,這也將成為本次圖層功能的基礎(chǔ) 既以一個group代表一個圖層,畫布下第一層children只有圖層(group),而在group中,才是用戶實際繪制的內(nèi)容
效果預(yù)覽
本次demo實現(xiàn):
- 用戶可手動添加/刪除圖層
- 可對每個圖層進行獨立顯隱操作,并反饋到畫布中
- 可對每個圖層單獨預(yù)覽
效果圖:

(別嫌我的樣式丑,我已經(jīng)下班了,沒有UI能夠PUSH我?。?/p>
下期預(yù)告
- 讓圖層能夠調(diào)整圖層層級
- 結(jié)合undo + redo + 橡皮擦
小Tips
首先fabric是需要到官方上下載的,在選擇你需要的模塊后再進行打包
雖然npm上也可以下載,但那不是官方的包,是有網(wǎng)友打包好以后上傳的,其中沒有包含橡皮擦模塊,很可能會不符合你的需求
所以我個人建議你可以自行去官網(wǎng)上打包,然后傳到你的私有npm庫里,然后就可以通過npm來管理了
fabric自定義打包下載地址:Custom Fabric build — Fabric.js Javascript Canvas Library (fabricjs.com)
fabric的事件文檔:Event inspector | Fabric.js Demos (fabricjs.com)
接下來的demo將會通過直接引入的方式來使用fabric,雖然我平時寫項目都是ts,但練手demo我個人建議還是js,問就是省事
完整代碼
目錄:
- fabric.js即為官網(wǎng)下載的插件包,這個文件就不放了,大家可以自行去官網(wǎng)打包下載
- index.html即本次的頁面,主要負責dom的處理,直接扔瀏覽器運行就可以
- sketchpad.js 是對fabric的二次封裝,同時也避免在html中寫太多fabric功能代碼

index.html代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
.content {
display: flex;
}
.preview {
margin-top: 40px;
padding-top: 20px;
width: 100%;
display: flex;
justify-content: center;
border-top: 1px solid rgba(0,0,0,0.1);
}
.preview > img {
width: 300px;
height: 200px;
}
.layer-list {
width: 300px;
display: flex;
flex-direction: column;
padding-right: 10px;
margin-right: 10px;
box-sizing: content-box;
border-right: 1px solid rgba(0,0,0, 0.2);
}
.layer {
width: 300px;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
border: 1px solid rgba(0,0,0, 0.2);
}
.layer > img {
width: 30px;
height: 30px;
border: 1px solid rgba(0,0,0,0.1);
}
</style>
</head>
<body>
<div id="app">
<div class="content">
<!-- 左側(cè)的圖層列表 -->
<div class="layer-list">
<button style="margin-bottom: 20px;" @click="addLayer">增加圖層</button>
<div @click="changeCurrentLayer(item.id)" class="layer" :style="currentLayer === item.id ? 'background-color: rgba(0,0,0, 0.1)' : '' " v-for="item of layers" :key="item.id">
<button @click="changeVisible(item.id)">{{ item.show ? '已顯示' : '已隱藏'}}</button>
<img :src="item.data">
<span>{{ item.name }}</span>
<button @click="deleteLayer(item.id)">刪除</button>
</div>
</div>
<!-- 右側(cè)的畫板 -->
<div class="sketchpad-layout" style="width: 600px;">
<canvas id="sketchpad" width="600" height="400" style="border: 1px solid #ccc;"></canvas>
</div>
</div>
<!-- 對整張畫布進行圖片預(yù)覽 -->
<div class="preview">
<button @click="updatePreview">整個畫布預(yù)覽:</button>
<img :src="preview">
</div>
</div>
<!-- 使用vue3來進行ui的渲染,懶得操作dom了 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- 打包好的fabric文件 -->
<script src="./fabric.js"></script>
<!-- sketchpad里面就是將fabric封裝了一層 -->
<script src="./sketchpad.js"></script>
<script>
const { createApp } = Vue
/** 單條的數(shù)據(jù)類型定義 */
const LayerData = {
/** 用于顯示的名稱 */
name: '圖層名稱',
/** 圖層的id,也用于管理圖層 */
id: '1111',
/** 圖層的顯示狀態(tài) */
show: true,
/** 圖層的數(shù)據(jù),用于顯示預(yù)覽圖 */
data: '',
}
createApp({
data() {
return {
layers: [],// 圖層數(shù)組,方便管理
sketchpad: null,// 畫板
currentLayer: '',// 當前圖層的id
preview: '',// 預(yù)覽圖的base64數(shù)據(jù)
}
},
methods: {
/**
* 改變圖層的顯示/隱藏
* @param id 圖層的id
*/
changeVisible(id) {
const index = this.layers.findIndex(v => v.id === id);
if (index > -1) {
this.layers[index].show = !this.layers[index].show;
}
this.sketchpad.changeLayerVisible(id)
},
/**
* 刪除圖層
* @param id 圖層的id
*/
deleteLayer(id) {
const index = this.layers.findIndex(v => v.id === id);
if (index > -1) {
this.layers.splice(index, 1)
this.sketchpad.deleteLayer(id)
}
},
/**
* 增加圖層
*/
addLayer() {
const item = {
...LayerData
}
item.id = new Date().getTime()
item.name = `圖層${this.layers.length + 1}`
this.layers.push(item)
this.sketchpad.addLayer(item.id)
this.changeCurrentLayer(item.id)
},
/** 選擇當前要操作的圖層 */
changeCurrentLayer(id) {
this.currentLayer = id
this.sketchpad.changeCurrentLayer(id)
},
/**
* 更新預(yù)覽圖
*/
updatePreview() {
this.preview = this.sketchpad.getImage()
},
/** 圖層數(shù)據(jù)更新的回調(diào) */
onChangeData(id, data) {
const index = this.layers.findIndex(v => v.id === id);
if (index > -1) {
this.layers[index].data = data;
}
}
},
mounted() {
this.sketchpad = new Sketchpad('sketchpad', {
change: this.onChangeData
});
this.addLayer()
}
}).mount('#app')
</script>
</body>
</html>sketchpad.js代碼
console.log('Sketchpad load');
class Sketchpad {
/** fabric實例 */
instance = null;
/** 當前所在圖層的id */
currentLayer = '';
/** 畫布寬度 */
width = 600;
/** 畫布高度 */
height = 600
/** 事件訂閱 */
listeners = {
/**
* 圖層內(nèi)容變化時的回調(diào)
* @param {string} id 圖層id
* @param {base64} data 圖層內(nèi)容的base64格式
*/
change: (id, data) => {}
}
constructor(id, listeners) {
this.instance = new fabric.Canvas(id);
this.width = this.instance.width;
this.height = this.instance.height;
this.instance.isDrawingMode = true;
this.listeners.change = listeners.change
this.instance.on('object:added', ((options) => {
if (options.target.type === 'group') return;
const groups = this.instance.getObjects()
groups.forEach(v => {
if (v.layerId === this.currentLayer && v.type === 'group') {
v.addWithUpdate(options.target);
this.instance.remove(options.target);
this.listeners.change(v.layerId, v.toDataURL({
width: this.width,
height: this.height
}))
}
})
}))
console.log('Sketchpad init')
}
/** 添加圖層 */
addLayer(id) {
const group = new fabric.Group([], {
width: this.width,
height: this.width,
});
// 在這里增加一個自定義屬性 layerId ,用于區(qū)分圖層
group.layerId = id
this.instance.add(group)
this.currentLayer = id;
this.listeners.change(id, group.toDataURL({
width: this.width,
height: this.height
}))
}
/** 改變圖層的顯示/隱藏 */
changeLayerVisible(id) {
const groups = this.instance.getObjects()
groups.forEach(v => {
if (v.layerId === id && v.type === 'group') {
v.visible = !v.visible;
this.instance.renderAll() // 刷新畫布,改變group的visible屬性,必須通過刷新畫布,才能應(yīng)用新屬性值
}
})
}
/** 選擇要操作的圖層 */
changeCurrentLayer(id) {
this.currentLayer = id
}
/** 刪除圖層 */
deleteLayer(id) {
const groups = this.instance.getObjects()
groups.forEach(v => {
if (v.layerId === id && v.type === 'group') {
this.instance.remove(v)
this.instance.renderAll() // 刷新畫布
}
})
}
/** 獲取畫布數(shù)據(jù),以img標簽可以識別的base64格式 */
getImage() {
return this.instance.toDataURL()
}
}
將以上這兩個文件代碼直接復(fù)制粘貼到編輯器里,然后再去打包個fabric.js也放進編輯器里,就可以運行啦
以上就是fabric.js圖層功能獨立顯隱 添加 刪除 預(yù)覽實現(xiàn)詳解的詳細內(nèi)容,更多關(guān)于fabric.js圖層功能實現(xiàn)的資料請關(guān)注腳本之家其它相關(guān)文章!

