使用 fabric.js 开发移动端 H5 图片编辑器
使用 fabric.js 开发移动端 H5 图片编辑器
大家好,我是开源图片编辑器的 https://github.com/ikuaitu/vue-fabric-editor 的作者,它是一款基于 PC 版本的开源图片编辑器。
最近很多开发者咨询,是否可以将开源图片编辑器改造为一款适用于移动端的 H5 版本图片编辑器,最近 H5 版本的图片编辑器刚刚上线,就将实现思路和产品细节整理成笔记分享出来,供大家参考。
基础
开源的图片编辑器的基本功能都有了,例如切换模板、添加元素、自定义字体等,不过相较于移动端的交互会有很大的差异,做了很多改造,这次笔记主要分享一下移动端图片编辑器实现思路和细节。
大纲
- 切换模板
- 添加图片
- 添加组合元素
- 设置背景色
- 修改画布尺寸
- 快捷菜单
- 属性工具条
- 特效字体
- 切换字体
- 输入文字
- 文字排版
- 边框
- 阴影
- 下载图片
注:部分代码示例为封装后的代码,非 fabric.js 原生方法。
1. 切换模板
编辑器基于 fabric.js 开发,所有的模板都是以 json 的格式存储,切换模板只需要请求详情接口,将 json 格式的数据调添加到画布当中即可,需要注意的点是需要将模板中使用的字体名称,并加载字体文件后再进行渲染,否则字体样式没办法正常渲染。
const loadInfo = async (res: any) => {
const info = res.data
templName.value = info.name;
await canvasEditor.getFontList(JSON.stringify(info.json));
canvasEditor.loadJSON(JSON.stringify(info.json), () => LoadingPlugin(false));
};
2. 添加图片
fabric.js 中添加图片提供了很多种方法,我们使用通过最简单的fabric.Image.fromURL
即可,另外,经常有图片尺寸大于画布的情况,还需要将图片按画布宽度的一般进行缩放,更方便用户操作。
const toEditor = async (e: MouseEvent) => {
visible.value = false
LoadingPlugin(true)
const item = await canvasEditor.createImgByElement(e.target as HTMLImageElement)
await canvasEditor.addBaseType(item, { scale: true })
LoadingPlugin(false)
}
3. 添加组合元素
fabric.js 支持将单个元素按照 JSON 格式导出/导入,我们将导出的数据存储在数据库中的,导入时按元素类型导入即可,需要获取 JSON 中元素的类型,并作为方法名调用,同样需要在导入前做字体加载,倒入后做缩放。
const capitalizeFirstLetter = (str: string) => {
return str.charAt(0).toUpperCase() + str.slice(1);
}
const toEditor = async (item: ItemProps) => {
visible.value = false
LoadingPlugin(true)
await canvasEditor.downFontByJSON(JSON.stringify(item.json));
const el = JSON.parse(JSON.stringify(item.json));
const elType = capitalizeFirstLetter(el.type);
new fabric[elType].fromObject(el, (fabricEl: fabric.Object) => {
canvasEditor.dragAddItem(fabricEl);
LoadingPlugin(false)
});
}
4. 设置背景色
设置背景色较为简单,按照 fabric.js 的 API 设置颜色即可,需要注意的是大部分 PC 端的颜色组件并不适配移动端 H5 的场景,不支持 touch 事件,我们使用了 @jaames/iro
这个组件,它在移动端表现出色,完全适配我们的场景,而且它的 API 很灵活,我们将它封装成一个通用的颜色组件,在多处调用。
<template>
<div ref="pickerContainer">
</div>
</template>
<script setup lang="ts">
import iro from '@jaames/iro';
const emit = defineEmits(['update:modelValue', 'change']);
const props = defineProps({
modelValue: {
type: String,
default: '#000000'
},
width: {
type: Number,
default: 200
}
});
const pickerContainer = ref<HTMLButtonElement | string>('');
let colorPicker: any = null;
onMounted(() => {
// 创建iro.js颜色选择器
colorPicker = iro.ColorPicker(pickerContainer.value, {
width: props.width,
color: props.modelValue,
borderWidth: 1,
borderColor: "#fff",
layoutDirection: 'horizontal',
layout: [
{
component: iro.ui.Slider,
options: {
id: 'hue-slider',
sliderType: 'hue'
}
},
{
component: iro.ui.Box,
},
{
component: iro.ui.Slider,
options: {
sliderType: 'alpha'
}
}
]
});
// 监听颜色变化事件并发射自定义事件
colorPicker.on('color:change', (color: any) => {
const rgbaString = color.rgbaString;
emit('update:modelValue', rgbaString);
emit('change', rgbaString);
});
});
</script>
5. 修改画布尺寸
日常使用图片编辑器都有修改画布尺寸的需要,在开源项目中已经封装好了相应的方法,直接调用即可,需要注意的是,当修改尺寸弹框弹出时,为了达到所见即所得的效果,要避免弹框遮挡画布,其他属性修改同理。
const resizeEditor = async () => {
await nextTick()
const editorWorkspase = document.querySelector('#workspace') as HTMLElement
const popElement = document.querySelector('.my-editor-popup') as HTMLElement
const headerElement = document.querySelector('.t-navbar') as HTMLElement
if (popElement) {
editorWorkspase.style.height = `calc(100vh - ${popElement?.offsetHeight + headerElement?.offsetHeight || 0}px)`
} else {
editorWorkspase.style.height = ''
}
}
6. 快捷菜单
很多快捷操作需要能够让用户快速找的并完成操作,我们为元素添加了快捷菜单功能,避免让一些简单的操作让用户在底部菜单栏点来点去,当选中元素时自动展示,取消选中时隐藏即可,需要注意的是在快捷菜单并不总是在元素上方,快捷菜单应该根据元素位置和画布的尺寸进行定位,当菜单超出画布区域时我们要及时调整菜单位置;另外 当属性弹框出现,画布尺寸变化时,需要同步修改菜单位置。
// 更新位置信息
const upDatePosition = async () => {
const activeObject = canvasEditor.canvas.getActiveObject();
if (activeObject) {
canvasEditor.canvas.renderAll();
fixLeft.value = 10;
fixTop.value = 10;
await nextTick();
isIncluded(activeObject);
await nextTick();
}
}
// 监听选中对象变化更新位置信息
getObjectAttr(upDatePosition)
canvasEditor.canvas.on('selection:updated', upDatePosition)
canvasEditor.canvas.on('mouse:move', upDatePosition)
canvasEditor.on('workspaceAutoEvent', upDatePosition)
7. 属性工具条
参考了其他图片编辑器,部分属性在点击元素后才会出现可修改选项,取消选中时便隐藏选项,另外 选中的元素不同,可修改选项也不同,这是一个在移动端做复杂图片编辑器中非常棒的一个交互。
我们封装了通用的选中类型和方法,针对每个属性组件单独设置隐藏/展示。
8. 特效字体
特效字体主要是文字元素的颜色、边框、阴影的组合,我们将来文字设置样式后的 JSON 导出并保存在数据库中,当选中某一个特效时,将属性按 JSON 中的数据设置给元素即可。
const setStyle = (item: ImgItem) => {
const activeObject = canvasEditor.canvas.getActiveObjects()[0];
if (activeObject) {
const values = toRaw(item.json);
const keys = ['fill', 'stroke', 'strokeWidth', 'shadow', 'strokeLineCap'];
activeObject.set('paintFirst', 'stroke');
keys.forEach((key) => {
activeObject.set(key, values[key]);
if (key === 'fill' && typeof values[key] != 'string') {
activeObject.set(key, new fabric.Gradient(values[key]));
}
});
canvasEditor.canvas.renderAll();
}
};
9. 切换字体
修改字体只需要调用 fabric.js 元素的fontFamily
属性即可,在修改之前要确保字体加载完成。
const changeCommon = async (key: string, value: any) => {
const activeObject = canvasEditor.canvas.getActiveObjects()[0];
if (activeObject) {
LoadingPlugin(true);
baseAttr.fontFamily = value;
try {
await canvasEditor.loadFont(value)
} catch (error) {
console.log(error)
}
LoadingPlugin(false);
activeObject && activeObject.set(key, value);
canvasEditor.canvas.renderAll();
}
};
10. 输入文字
fabric.js 可直接双击文字元素进行修改,不过在移动端这种交互并不醒目,我们单独为文本元素进行了修改,选中元素后,再次点击时弹出输入框,可以在底部菜单栏点击按钮进行修改。
11. 文字排版
文字排版较为简单,我们只需要按照 fabric.js 的文字属性对文字进行属性设置即可,如 fontSize、lineHeight、charSpacing 等。
// 属性值
const baseAttr = reactive({
fontSize: 0,
lineHeight: 0,
charSpacing: 0,
textAlign: '',
fontWeight: '',
fontStyle: '',
underline: false,
linethrough: false,
overline: false,
});
12. 边框
边框样式和文字样式类似,配合颜色组件可以很快捷的实现功能。
// 属性值
const baseAttr = reactive({
stroke: '#fff',
strokeWidth: 0,
strokeDashArray: [],
});
13. 阴影
引用属性主要是元素的 shadow 子属性的修改,代码如下:
// 属性值
const baseAttr = reactive({
shadow: {
color: '#fff',
blur: 0,
offsetX: 1,
offsetY: 1,
}
});
// 通用属性改变
const changeCommon = () => {
const activeObject = canvasEditor.canvas.getActiveObjects()[0];
if (activeObject) {
activeObject.set('shadow', new fabric.Shadow(baseAttr.shadow));
canvasEditor.canvas.renderAll();
}
};
14. 下载图片
fabric.js 可以导出 Png/Jpeg/Base64 格式的图片,同时 JPEG 格式还可以指定图片质量与尺寸倍数,详见 fabric.js 的 API 文档。
结尾
以上就是 fabric.js 开发移动端编辑器的实现细节了,结合我们的开源项目和插件化架构可以很方便的完成项目开发,如果你在做类似项目或者做类似的项目,欢迎与我交流。
开源项目:https://github.com/ikuaitu/vue-fabric-editor/blob/main/README-zh.md