前言
通過vue2和fabric.js實現(xiàn)一個簡易的圖文編輯器,可以在畫布上添加文字,圖片,設(shè)置背景圖,對文字,圖片的屬性進(jìn)行修改。最后生成圖片。至于畫布上對選中的對象進(jìn)行拖動,縮放,旋轉(zhuǎn),這些能力fabric本身已經(jīng)支持。
1 創(chuàng)建一個vue項目
2 安裝fabric.js
建議使用4或5版本,最新版學(xué)習(xí)成本較高,相關(guān)經(jīng)驗文檔少。
npm install fabric@4.6.0
3核心代碼
頁面基本結(jié)構(gòu)
頁面左側(cè)為添加元素區(qū)域,可添加文字,圖片等元素。中間為畫布。右側(cè)對選中的元素的屬性進(jìn)行修改。
具體代碼可參考源碼。項目中我用的node版本是18.17.1。
<div class="editor">
<div class="sidebar left">
<button @click="addText">添加文本</button>
<button @click="addImage">添加圖片</button>
<button @click="setBackgroundImage">添加背景圖</button>
</div>
<canvas id="c" width="600" height="600" class="canvas"></canvas>
<div class="sidebar right">
<!-- 右側(cè)屬性面板 -->
<div v-if="selectedObject">
屬性修改...
</div>
</div>
</div>
初始化畫布
首先確保 頁面中已經(jīng)有canvas標(biāo)簽。
data中定義需要用到到參數(shù)
data() {
return {
canvas: null,
selectedObject: null, // 當(dāng)前選中的元素對象
canvasWidth: 800, // 初始畫布寬度
canvasHeight: 600, // 初始畫布高度
canvasBackgroundColor: '#FFF', // 初始畫布背景色
};
},
在mounted鉤子函數(shù)中創(chuàng)建了Fabric.js畫布并監(jiān)聽鼠標(biāo)點擊
創(chuàng)建fabric畫布,并指定背景色,大小。
監(jiān)聽mouse:up事件,點擊畫布上的元素時,更新selectedObject,selectedObject對象表示當(dāng)前選中元素的屬性。
mounted() {
this.$nextTick(() => {
this.initCanvas();
})
},
initCanvas() {
// 創(chuàng)建畫布
this.canvas = new fabric.Canvas('c',{
backgroundColor: this.canvasBackgroundColor,
width: 800,
height: 576,
});
// 監(jiān)聽點擊
this.canvas.on('mouse:up', (e) => {
if (e.target) {
this.selectedObject = e.target;
} else {
this.selectedObject = null
}
});
},
添加文本
addText() {
const text = new fabric.IText('點擊編輯', {
left: 100,
top: 100,
fontSize: 30,
fontFamily: 'arial', // 字體
fill: '#333', // 顏色
originX: 'left',
originY: 'top',
});
this.canvas.add(text);
},
以上只添加了文本的基本屬性,除此之外還有一些常用屬性:
editable:是否可編輯,值為布爾值;
lockUniScaling:控制四個正方向縮放,值為布爾值;
lockScalingX: 禁止橫向縮放,值為布爾值;
lockScalingY: 禁止縱向縮放,值為布爾值;
同時還可以添加自定義的屬性,例如我在添加文本元素時,自定義了屬性system_name。
addText() {
const text = new fabric.IText('當(dāng)前日期', {
......
system_name: 'current_date',
});
this.canvas.add(text);
},
添加圖片
addImage() {
fabric.Image.fromURL('圖片url', (img) => {
img.set({
left: 100,
top: 100,
angle: 0, // 你可以根據(jù)需要調(diào)整圖片的旋轉(zhuǎn)角度
});
// 將圖片添加到畫布
this.canvas.add(img);
// 重新渲染畫布以顯示新添加的圖片
this.canvas.renderAll();
}, { crossOrigin: 'anonymous' });
},
fabric.Image.fromURL('圖片url', callback, options) 方法用于從指定的 URL 加載圖片。這個方法接受三個參數(shù)。
'圖片url':圖片的 URL 地址。
callback(img):一個回調(diào)函數(shù),當(dāng)圖片加載完成后執(zhí)行。img 參數(shù)是加載后的 Fabric.js 圖片對象。
options:一個對象,包含加載圖片時的選項。在這個例子中,設(shè)置了 { crossOrigin: 'anonymous' },這允許跨域加載圖片,避免在加載跨域圖片時出現(xiàn) CORS(跨源資源共享)錯誤。
如果我希望添加的圖片不超出畫布,同時居中。上述代碼可以這樣改進(jìn):
計算縮放因子scale并應(yīng)用,保證圖片最長的邊不會超出畫布。
addImage() {
fabric.Image.fromURL('圖片url', (img) => {
// 獲取畫布的寬高
const canvasWidth = this.canvas.getWidth();
const canvasHeight = this.canvas.getHeight();
const maxWidth = canvasWidth * 0.6; // 計算圖片允許的最大寬度
const maxHeight = canvasHeight * 0.3; // 計算圖片允許的最大高度
// 計算縮放比例
let scale = Math.min(maxWidth / img.width, maxHeight / img.height);
// 應(yīng)用縮放比例
img.scale(scale).set({
left: (canvasWidth - img.width * scale) / 2, // 居中圖片
top: (canvasHeight - img.height * scale) / 2,
});
// 將圖片添加到畫布
this.canvas.add(img);
// 重新渲染畫布以顯示新添加的圖片
this.canvas.renderAll();
}, { crossOrigin: 'anonymous' });
},
設(shè)置背景圖
設(shè)置背景圖有兩種實現(xiàn)方式,
1 通過canvas.setBackgroundImage()設(shè)置;
2 添加一個圖片,寬高與畫布大小一致,將其放在所有其他對象的底層,并禁止選中、觸發(fā)事件。
我這里使用的是第二種方法。
setBackgroundImage(imageUrl) {
fabric.Image.fromURL(imageUrl, (img) => {
// 獲取畫布的寬度和高度
const canvasWidth = this.canvas.getWidth();
const canvasHeight = this.canvas.getHeight();
// 直接設(shè)置圖像的寬度和高度為畫布的寬度和高度
img.set({
left: 0,
top: 0,
scaleX: canvasWidth / img.width,
scaleY: canvasHeight / img.height,
selectable: false, // 讓背景圖不可選
evented: false, // 讓背景圖不觸發(fā)事件
is_background: true // 自定義屬性,背景圖標(biāo)識 區(qū)別于普通圖片元素
});
// 將背景圖添加到畫布上
this.canvas.add(img);
// 將背景圖放在所有其他對象的底層
this.canvas.sendToBack(img);
// 重新渲染畫布
this.canvas.renderAll();
}, { crossOrigin: 'anonymous' });
},
上述方法可以實現(xiàn)設(shè)置背景圖,但如果我已經(jīng)設(shè)置了背景圖,現(xiàn)在又想替換其他背景圖時。由于新添加的背景圖被放到了最底層,舊的背景圖還沒刪除掉,舊圖覆蓋在新的背景圖上,所以上述代碼需要優(yōu)化,
解決方法:
因為我在添加背景圖時自定義了屬性is_background,所以每次添加背景圖先前遍歷畫布上的元素,如果is_background屬性為true則刪除它。然后再添加背景圖。完整邏輯如下
setBackgroundImage(imageUrl) {
fabric.Image.fromURL(imageUrl, (img) => {
// 獲取畫布的寬度和高度
const canvasWidth = this.canvas.getWidth();
const canvasHeight = this.canvas.getHeight();
// 直接設(shè)置圖像的寬度和高度為畫布的寬度和高度
img.set({
left: 0,
top: 0,
scaleX: canvasWidth / img.width,
scaleY: canvasHeight / img.height,
selectable: false, // 讓背景圖不可選
evented: false, // 讓背景圖不觸發(fā)事件
is_background: true // 背景圖標(biāo)識
});
// 遍歷畫布上的所有對象,查找并刪除已存在的背景圖
this.canvas.getObjects().forEach((obj) => {
if (obj.is_background) {
this.canvas.remove(obj);
}
});
// 將背景圖添加到畫布上
this.canvas.add(img);
// 將背景圖放在所有其他對象的底層
this.canvas.sendToBack(img);
// 重新渲染畫布
this.canvas.renderAll();
}, { crossOrigin: 'anonymous' });
},
修改屬性
選中元素時,selectedObject表示選中的對象,此時右側(cè)顯示相應(yīng)的屬性值修改框。
通過selectedObject.type區(qū)分文本或圖片。i-text為文本,image為圖片
文字常見屬性修改:
字體顏色:fill;
字體大?。篺ontSize;
字體粗細(xì):fontWeight,常規(guī)為normal,加粗bold;
字體風(fēng)格:fontStyle,常規(guī)為normal,斜體italic;
下劃線:underline,布爾值;
刪除線:linethrough,布爾值;
<!-- 字體屬性修改 -->
<div v-if="selectedObject.type === 'i-text'">
<div class="style-title">顏色:</div>
<el-color-picker style="width: 100%;" v-model="selectedObject.fill" @change="updateColor"></el-color-picker>
<div class="style-title">字體大小:</div>
<el-input-number size="small" v-model="selectedObject.fontSize" controls-position="right" @change="canvasRender" :min="1"></el-input-number>
<div class="style-title">常用屬性:</div>
<!-- 加粗,斜體,下劃線,刪除線 -->
<div class="font-style">
<i class="fa fa-bold" @click="updateTextProps('bold')"></i>
<i class="fa fa-italic" @click="updateTextProps('italic')"></i>
<i class="fa fa-underline" @click="updateTextProps('underline')"></i>
<i class="fa fa-strikethrough" @click="updateTextProps('linethrough')"></i>
</div>
</div>
......
methods: {
// 修改顏色
updateColor(newColor) {
this.selectedObject.fill = newColor
this.selectedObject.dirty = true;
this.canvas.renderAll();
},
// 渲染畫布
canvasRender(e) {
this.canvas.renderAll();
},
// 修改文字屬性
updateTextProps(type) {
if(type == 'bold') {
// 加粗
this.selectedObject.fontWeight = this.selectedObject.fontWeight == 'normal' ? 'bold' : 'normal'
}
if(type == 'italic') {
// 斜體
this.selectedObject.fontStyle = this.selectedObject.fontStyle == 'normal' ? 'italic' : 'normal'
}
if(type == 'linethrough') {
// 刪除線
this.selectedObject.linethrough = !this.selectedObject.linethrough
}
if(type == 'underline') {
// 下劃線
this.selectedObject.underline = !this.selectedObject.underline
}
this.selectedObject.dirty = true;
this.canvas.renderAll();
},
}
圖片常見屬性修改
圖片我主要做了尺寸的修改。
直接修改width,height并不能改變圖片的大小。這是圖片本身的物理大小,不能修改的。畫布上展現(xiàn)的圖片實際大小為物理大小*縮放比,例如寬默認(rèn)為:width*scaleX,高為height*scaleY。修改圖片尺寸,實際上就是修改縮放比scale。因此,在添加圖片時,我需要為圖片添加兩個自定義屬性,來表示圖片在畫布上的實際大小。添加自定義屬性scaleWidth,scaleHeight,表示縮放后圖片的實際大小。上述添加圖片的方法可做以下優(yōu)化:
addImage() {
fabric.Image.fromURL('圖片url', (img) => {
......
img.scale(scale).set({
......
scaleWidth: img.width * scale,
scaleHeight: img.height * scale
});
......
}, { crossOrigin: 'anonymous' });
},
接下來,在右側(cè)屬性編輯區(qū)修改圖片尺寸,實際上要根據(jù)修改后的尺寸scaleWidth去計算新的縮放比scaleX,height同理。然后更新圖片屬性的scaleX,scaleY屬性即可。直接用鼠標(biāo)拖拽圖片的邊去進(jìn)行縮放也是在修改scaleX,scaleY。
<!-- 圖片屬性修改 -->
<div v-if="selectedObject.type === 'image'">
<div class="style-title">尺寸:</div>
<div class="place-line">
<el-input-number v-model="selectedObject.scaleWidth" size="small" placeholder="寬度" controls-position="right" @change="updateImgScale" :min="1" :max="1000" :precision="0" />
<el-input-number v-model="selectedObject.scaleHeight" size="small" placeholder="高度" controls-position="right" @change="updateImgScale" :min="1" :max="1000" :precision="0" />
</div>
</div>
methods: {
// 縮放圖片
updateImgScale() {
const newScaleX = this.selectedObject.scaleWidth / this.selectedObject.width;
const newScaleY = this.selectedObject.scaleHeight / this.selectedObject.height;
this.selectedObject.set({
scaleX: newScaleX,
scaleY: newScaleY,
})
this.canvas.renderAll();
},
}
刪除
將當(dāng)前選中的對象或?qū)ο蟮募蟿h除。我這里用到的是getActiveObjects,獲取所有選中的對象,遍歷并通過canvas.remove()全部刪除,如果想獲取單個對象,可以用this.canvas.getActiveObject()。
<i class="el-icon-delete" style="color: red;" @click="deleteSelectedObjects">
刪除</i>
......
// 刪除元素
deleteSelectedObjects() {
const selectedObjects = this.canvas.getActiveObjects();
if (selectedObjects.length > 0) {
selectedObjects.forEach(obj => {
this.canvas.remove(obj);
});
this.canvas.renderAll();
// 清除 selectedObject 引用,如果你需要在其他地方使用它
this.selectedObject = null;
}
},
生成JSON
將畫布上的所有內(nèi)容生成JSON文件。通過canvas.toJSON將畫布上的圖形對象轉(zhuǎn)為JSON對象,生成的JSON對象默認(rèn)情況下是包含對象的所有屬性,但自定義屬性我們需要手動指定。this.canvas.toJSON(['屬性A', '屬性B',...])
<el-button type="primary" size="small" @click="exportCanvasAsJSON">生成JSON</el-button>
......
exportCanvasAsJSON() {
// 獲取畫布上所有對象的JSON表示
const jsonData = this.canvas.toJSON(['selectable', 'evented', 'is_background', 'scaleX', 'scaleY', 'scaleWidth', 'scaleHeight']); // 你可以根據(jù)需要包含或排除屬性
// 將JSON對象轉(zhuǎn)換為字符串
const jsonString = JSON.stringify(jsonData, null, 2);
// 以下是下載的邏輯=====================
// 創(chuàng)建一個Blob對象
const blob = new Blob([jsonString], { type: 'text/json' });
// 創(chuàng)建一個指向blob的URL
const url = window.URL.createObjectURL(blob);
// 創(chuàng)建一個臨時的a標(biāo)簽用于下載
const a = document.createElement('a');
a.href = url;
a.download = 'canvas_data.json'; // 指定下載的文件名
document.body.appendChild(a);
a.click(); // 模擬點擊以觸發(fā)下載
// 清理
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
},
生成圖片
通過canvas.toDataURL將畫布內(nèi)容導(dǎo)出為數(shù)據(jù) URL,并指定為圖像格式(如 PNG 或 JPEG)。
<el-button type="primary" size="small" @click="exportCanvasAsImage">下載圖片</el-button>
......
exportCanvasAsImage() {
// 設(shè)置圖片的質(zhì)量和格式,這里以PNG格式為例,質(zhì)量為0.8
const imageUrl = this.canvas.toDataURL({
format: 'png',
quality: 0.8
});
// 以下是下載的邏輯=====================
// 創(chuàng)建一個指向該DataURL的a標(biāo)簽用于下載
const a = document.createElement('a');
a.href = imageUrl;
a.download = 'canvas_image.png'; // 指定下載的文件名
document.body.appendChild(a);
a.click(); // 模擬點擊以觸發(fā)下載
document.body.removeChild(a);
window.URL.revokeObjectURL(imageUrl);
}
渲染JSON為圖像
前面生成的JSON文件,我們通常會保存到本地或傳給后端。一般二次編輯時,是需要回顯畫布的?;仫@畫布可通過canvas.loadFromJSON(json, [callback])來實現(xiàn)。
json (String): 描述畫布狀態(tài)的 JSON 字符串。
callback (Function, 可選): 當(dāng) JSON 數(shù)據(jù)加載完成并渲染到畫布上后調(diào)用的函數(shù)
mounted() {
this.initData()
// 如果是編輯時
if(isEdit) {
// 請求接口,或讀取本地的JSON文件...
// jsonData為需要渲染的JSON
this.canvas.loadFromJSON(jsonData, this.canvas.renderAll.bind(this.canvas))
}
},
?轉(zhuǎn)自https://juejin.cn/post/7427513979639496741
該文章在 2024/10/25 10:41:56 編輯過