前言
通过vue2和fabric.js实现一个简易的图文编辑器,可以在画布上添加文字,图片,设置背景图,对文字,图片的属性进行修改。最后生成图片。至于画布上对选中的对象进行拖动,缩放,旋转,这些能力fabric本身已经支持。
1 创建一个vue项目
2 安装fabric.js
建议使用4或5版本,最新版学习成本较高,相关经验文档少。
npm install fabric@4.6.0
3核心代码
页面基本结构
页面左侧为添加元素区域,可添加文字,图片等元素。中间为画布。右侧对选中的元素的属性进行修改。
具体代码可参考源码。项目中我用的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">
<!-- 右侧属性面板 -->
<div v-if="selectedObject">
属性修改...
</div>
</div>
</div>
初始化画布
首先确保 页面中已经有canvas标签。
data中定义需要用到到参数
data() {
return {
canvas: null,
selectedObject: null, // 当前选中的元素对象
canvasWidth: 800, // 初始画布宽度
canvasHeight: 600, // 初始画布高度
canvasBackgroundColor: '#FFF', // 初始画布背景色
};
},
在mounted钩子函数中创建了Fabric.js画布并监听鼠标点击
创建fabric画布,并指定背景色,大小。
监听mouse:up事件,点击画布上的元素时,更新selectedObject,selectedObject对象表示当前选中元素的属性。
mounted() {
this.$nextTick(() => {
this.initCanvas();
})
},
initCanvas() {
// 创建画布
this.canvas = new fabric.Canvas('c',{
backgroundColor: this.canvasBackgroundColor,
width: 800,
height: 576,
});
// 监听点击
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('当前日期', {
......
system_name: 'current_date',
});
this.canvas.add(text);
},
添加图片
addImage() {
fabric.Image.fromURL('图片url', (img) => {
img.set({
left: 100,
top: 100,
angle: 0, // 你可以根据需要调整图片的旋转角度
});
// 将图片添加到画布
this.canvas.add(img);
// 重新渲染画布以显示新添加的图片
this.canvas.renderAll();
}, { crossOrigin: 'anonymous' });
},
fabric.Image.fromURL('图片url', callback, options) 方法用于从指定的 URL 加载图片。这个方法接受三个参数。
'图片url':图片的 URL 地址。
callback(img):一个回调函数,当图片加载完成后执行。img 参数是加载后的 Fabric.js 图片对象。
options:一个对象,包含加载图片时的选项。在这个例子中,设置了 { crossOrigin: 'anonymous' },这允许跨域加载图片,避免在加载跨域图片时出现 CORS(跨源资源共享)错误。
如果我希望添加的图片不超出画布,同时居中。上述代码可以这样改进:
计算缩放因子scale并应用,保证图片最长的边不会超出画布。
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);
// 应用缩放比例
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' });
},
设置背景图
设置背景图有两种实现方式,
1 通过canvas.setBackgroundImage()设置;
2 添加一个图片,宽高与画布大小一致,将其放在所有其他对象的底层,并禁止选中、触发事件。
我这里使用的是第二种方法。
setBackgroundImage(imageUrl) {
fabric.Image.fromURL(imageUrl, (img) => {
// 获取画布的宽度和高度
const canvasWidth = this.canvas.getWidth();
const canvasHeight = this.canvas.getHeight();
// 直接设置图像的宽度和高度为画布的宽度和高度
img.set({
left: 0,
top: 0,
scaleX: canvasWidth / img.width,
scaleY: canvasHeight / img.height,
selectable: false, // 让背景图不可选
evented: false, // 让背景图不触发事件
is_background: true // 自定义属性,背景图标识 区别于普通图片元素
});
// 将背景图添加到画布上
this.canvas.add(img);
// 将背景图放在所有其他对象的底层
this.canvas.sendToBack(img);
// 重新渲染画布
this.canvas.renderAll();
}, { crossOrigin: 'anonymous' });
},
上述方法可以实现设置背景图,但如果我已经设置了背景图,现在又想替换其他背景图时。由于新添加的背景图被放到了最底层,旧的背景图还没删除掉,旧图覆盖在新的背景图上,所以上述代码需要优化,
解决方法:
因为我在添加背景图时自定义了属性is_background,所以每次添加背景图先前遍历画布上的元素,如果is_background属性为true则删除它。然后再添加背景图。完整逻辑如下
setBackgroundImage(imageUrl) {
fabric.Image.fromURL(imageUrl, (img) => {
// 获取画布的宽度和高度
const canvasWidth = this.canvas.getWidth();
const canvasHeight = this.canvas.getHeight();
// 直接设置图像的宽度和高度为画布的宽度和高度
img.set({
left: 0,
top: 0,
scaleX: canvasWidth / img.width,
scaleY: canvasHeight / img.height,
selectable: false, // 让背景图不可选
evented: false, // 让背景图不触发事件
is_background: true // 背景图标识
});
// 遍历画布上的所有对象,查找并删除已存在的背景图
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表示选中的对象,此时右侧显示相应的属性值修改框。
通过selectedObject.type区分文本或图片。i-text为文本,image为图片
文字常见属性修改:
字体颜色:fill;
字体大小:fontSize;
字体粗细:fontWeight,常规为normal,加粗bold;
字体风格:fontStyle,常规为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并不能改变图片的大小。这是图片本身的物理大小,不能修改的。画布上展现的图片实际大小为物理大小*缩放比,例如宽默认为:width*scaleX,高为height*scaleY。修改图片尺寸,实际上就是修改缩放比scale。因此,在添加图片时,我需要为图片添加两个自定义属性,来表示图片在画布上的实际大小。添加自定义属性scaleWidth,scaleHeight,表示缩放后图片的实际大小。上述添加图片的方法可做以下优化:
addImage() {
fabric.Image.fromURL('图片url', (img) => {
......
img.scale(scale).set({
......
scaleWidth: img.width * scale,
scaleHeight: img.height * scale
});
......
}, { crossOrigin: 'anonymous' });
},
接下来,在右侧属性编辑区修改图片尺寸,实际上要根据修改后的尺寸scaleWidth去计算新的缩放比scaleX,height同理。然后更新图片属性的scaleX,scaleY属性即可。直接用鼠标拖拽图片的边去进行缩放也是在修改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();
},
}
删除
将当前选中的对象或对象的集合删除。我这里用到的是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
将画布上的所有内容生成JSON文件。通过canvas.toJSON将画布上的图形对象转为JSON对象,生成的JSON对象默认情况下是包含对象的所有属性,但自定义属性我们需要手动指定。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']); // 你可以根据需要包含或排除属性
// 将JSON对象转换为字符串
const jsonString = JSON.stringify(jsonData, null, 2);
// 以下是下载的逻辑=====================
// 创建一个Blob对象
const blob = new Blob([jsonString], { type: 'text/json' });
// 创建一个指向blob的URL
const url = window.URL.createObjectURL(blob);
// 创建一个临时的a标签用于下载
const a = document.createElement('a');
a.href = url;
a.download = 'canvas_data.json'; // 指定下载的文件名
document.body.appendChild(a);
a.click(); // 模拟点击以触发下载
// 清理
document.body.removeChild(a);
window.URL.revokeObjectURL(url);
},
生成图片
通过canvas.toDataURL将画布内容导出为数据 URL,并指定为图像格式(如 PNG 或 JPEG)。
<el-button type="primary" size="small" @click="exportCanvasAsImage">下载图片</el-button>
......
exportCanvasAsImage() {
// 设置图片的质量和格式,这里以PNG格式为例,质量为0.8
const imageUrl = this.canvas.toDataURL({
format: 'png',
quality: 0.8
});
// 以下是下载的逻辑=====================
// 创建一个指向该DataURL的a标签用于下载
const a = document.createElement('a');
a.href = imageUrl;
a.download = 'canvas_image.png'; // 指定下载的文件名
document.body.appendChild(a);
a.click(); // 模拟点击以触发下载
document.body.removeChild(a);
window.URL.revokeObjectURL(imageUrl);
}
渲染JSON为图像
前面生成的JSON文件,我们通常会保存到本地或传给后端。一般二次编辑时,是需要回显画布的。回显画布可通过canvas.loadFromJSON(json, [callback])来实现。
json (String): 描述画布状态的 JSON 字符串。
callback (Function, 可选): 当 JSON 数据加载完成并渲染到画布上后调用的函数
mounted() {
this.initData()
// 如果是编辑时
if(isEdit) {
// 请求接口,或读取本地的JSON文件...
// jsonData为需要渲染的JSON
this.canvas.loadFromJSON(jsonData, this.canvas.renderAll.bind(this.canvas))
}
},
转自https://juejin.cn/post/7427513979639496741
该文章在 2024/10/25 10:41:56 编辑过