今天跟后端的一个小伙伴抽烟。
他说:“前端的一粒灰落到我头上就是一座山”
我说:“此话怎讲”,我暗中好奇!
他说:”现在有个需要根据后端生成的数据,生成自定义的海报,后期可以简单的编辑,数据分为描述信息,二维码和表格信息”(虽说我是个前端,因为我们后台前后端没分离,而且仅剩的俩个前端还在干需求...)。
他用手机给我看了看需求文档,不禁长叹一口气。
我说:“我查查看看有什么好的方式实现”。这个时候我想到canvas,但是原生canvas的API太难用了,没有事件机制,图层和导出运行时当前的状态等等。等这些全实现今年的KPI估计也是凉了,我就在awesome-canvas上找库,找了一圈不出所料`Fabricjs`就是我想要的啊。
我以前也没有接触过关于canvas,趁这次机会我准备六日我想准备实现个demo看看。
首先它包括(其实它还有很多功能,但这里只提到我涉及的)
- 封装了canvas的API,操作起来更方便
- 有加载svg的解析器
- 事件机制和图层概念
- 支持导出和解析序列化当前状态
- 基本的编辑元素功能
这是大概要实现的效果(素材是我随便在网上找的)
文字生成
fabric.Text
生成文字
fabric.IText
生成可以编辑的文字
fabric.Textbox
基于 IText 的 Textbox 类允许用户自动调整文本矩形的大小和换行。锁定其 y 缩放,用户只能改变宽度,高度会根据线条的环绕自动调整
这里我选用fabric.IText
let canvas = new fabric.Canvas('c');
let text = new fabric.IText('hello world', { fontFamily: 'Comic Sans'});
canvas.add(text);
图片渲染
let canvas = new fabric.Canvas('c');
fabric.Image.fromURL('example.jpg', function(img) {
img.filters.push(
new fabric.Image.filters.Sepia(),
new fabric.Image.filters.Brightness({ brightness: 100 }));
img.applyFilters();
canvas.add(img);
});
编辑
上面的其实就是根据用户的输入配置生成相应的素材的拖动到相应的位置
//开启拖拽移动
canvas.selection = true
document.getElementById('id').addEventListener('click', (e) => {
//通过getObjects获取canvas想修改的图层的Index,修改上面的属性
canvas.item(0)['fill'] = e.value;
canvas.requestRenderAll();
})
表格生成
生成表格相对麻烦些,最后的预期类似这样
首先我们先整理我们要实现的表格中的功能
整体设计思路
- 根据mock数据生成相应的文本对象
- 根据上面计算的数值,画出表格
- 根据当前表格位置和文本对象属性,把文本放入到应属于文本的单元格中
- 最后整体渲染出来
根据mock数据生成相应的文本对象
let mockTable = [
['日期', '时间', '课程内容\n课程内容'],
['8月16号', '10:00-12:00', '1.课程内容课程内容课程内容课程内容课程内容课\n2.程程内容课程内容课程内容课程内容课\n3.程内容内容容课程内容课程内容课\n4.程内容课程内容课程内容课程内容课程内容课程内容课程内容课程内容'],
['8月16号', '10:00-12:00', '1.课程内容课程内容课程内容课程内容课程内容课\n2.程内容课程内容课程内容课程内容课程内容课程内容课程内容课程内容'],
]
//表格信息
let tableInfo = []
function createText(text) {
return new fabric.IText(text, {
fontSize: 30,
fontWeight: 400,
});
}
//初始化
mockTable.forEach((row, i) => {
tableInfo[i] = []
row.forEach((col, j) => {
mockTable[i][j] = createText(mockTable[i][j])
//设置标头字体
if (i === 0) {
mockTable[i][j].set('fontSize', 40)
mockTable[i][j].set('fontWeight', 700)
mockTable[i][j].set('fill', 'rgb(178,86,49)')
}
tableInfo[i][j] = {
width:0,
height:0
}
})
})
根据上面文字对象属性计算数值,画出表格
//设置临时数组方便,后期一次性渲染
let list = []
for (let i = 0; i < mockTable.length; i++) {
for (let j = 0; j < mockTable[0].length; j++) {
tableInfo[i][j].width = getWidth(j)
tableInfo[i][j].height = getHeight(i)
let [x, y] = getPostion(j, i)
let rect = new fabric.Rect({
left: x,
top: y,
//设置标头背景样式
fill: i === 0 ? 'rgb(231,225,186)' : 'white',
width: tableInfo[i][j].width,
height: tableInfo[i][j].height,
stroke: 'rgb(161,127,123)', strokeWidth: 2,
})
// 设置文字垂直居中
mockTable[i][j].set('left', x + (tableInfo[i][j].width - mockTable[i][j].width) / 2)
mockTable[i][j].set('top', y + (tableInfo[i][j].height - mockTable[i][j].height) / 2)
list.push(rect)
list.push(mockTable[i][j])
}
}
//设置图层方便编辑
let group = new fabric.Group(list, {
left: 100,
top: 100
});
canvas.selection = true
group.set('selectable', true);
canvas.add(group);
工具函数
//计算列最大宽度
function getWidth(x) {
let maxWidth = mockTable[0][x].width;
for (let i = 0; i < mockTable.length; i++) {
maxWidth = Math.max(maxWidth, mockTable[i][x].width)
}
return maxWidth
}
//计算行最大高度
function getHeight(y) {
let maxHeight = mockTable[y][0].height;
for (let i = 0; i < mockTable[0].length; i++) {
maxHeight = Math.max(maxHeight, mockTable[y][i].height)
}
return maxHeight
}
//累加行列宽高计算坐标
function getPostion(x, y) {
let x1 = 0;
let y1 = 0;
for (let i = 0; i < x; i++) {
x1 += tableInfo[y][i].width
}
for (let j = 0; j < y; j++) {
y1 += tableInfo[j][x].height
}
return [x1, y1]
}
撤销和前进
撤销和前进主要维护两个栈
- 初始化的时候,把当前的保存历史栈中,确保一直在最底下不被删除
- 在操作的时候清空前进的临时栈并且加入历史栈中
- 前进和撤销,把栈顶元素移动到另个栈上
let popDom = document.querySelector('#pop');
let pushDom = document.querySelector('#push');
let historyList = [JSON.stringify(canvas)];
let tempHistoryList = [];
canvas.on('mouse:up:before', function () {
tempHistoryList = []
historyList.push(JSON.stringify(canvas))
});
popDom.addEventListener('click', () => {
if (historyList.length === 1) {
canvas.clear()
canvas.loadFromJSON(JSON.parse(historyList[0])).renderAll()
return
}
tempHistoryList.push(JSON.parse(historyList.pop()))
let historyAction = historyList[historyList.length - 1];
canvas.clear()
canvas.loadFromJSON(JSON.parse(historyAction)).renderAll()
})
pushDom.addEventListener('click', () => {
if (tempHistoryList.length === 0) {
alert('无前进记录')
return
}
let tempAction = JSON.stringify(tempHistoryList.pop());
historyList.push(tempAction)
canvas.clear()
canvas.loadFromJSON(JSON.parse(tempAction)).renderAll()
})
最后的效果
优化
- 关于表格的数据结构设置可以加入单元格居中方式,颜色等等,好的数据结构也是方便以后功能的拓展。
- 在计算宽度的时候就有重复计算,其实可以对比没有最大宽度的时候直接取计算好的属性等方式。
- 在撤销功能那块,其实每次存储当前状态很占内存的,尤其是在加载图片的canvas。这里面可以锁定栈的大小多余的只取最新的,也可以优化
Fabricjs
导出json格式的大小。 - 在渲染方面,尽量应该对比和利用原始的对象进行修改在渲染对应的,不然每次都创
Fabric.object
还是可能造成占用内存过多和重复生成的计算时间。
其实对于现在来说优化空间很多的,大家有什么好的建议也欢迎一起讨论啊~,我也是第一次用canvas的有什么不对的地方麻烦请指正。
如果小伙伴对源码感谢在这里 github.com/cdhhhhhhh/f…