前言
本文将会带着大家基于React搭建一套属于自己的绘图工具。
环境搭建
初始化项目
create-react-app project
安装依赖
"@topology/activity-diagram": "^0.2.24",
"@topology/chart-diagram": "^0.3.0",
"@topology/class-diagram": "^0.2.24",
"@topology/core": "^0.3.1",
"@topology/flow-diagram": "^0.2.24",
"@topology/layout": "^0.3.0",
"@topology/sequence-diagram": "^0.2.24",
"antd": "^3.26.7",
至于基础布局的代码, 大家可以自由发挥, 本文就不赘述了。ok! 完成项目基本环境的搭建后, 就可以开始逐个完成以下功能点了。
功能介绍
自定义图片示例
主页面左侧的图形渲染区域, 可以自定义渲染。
const Layout = ({ Tools, onDrag }) => {
return Tools.map((item, index) => (
"title">{item.group}
"button">
{item.children.map((item, idx) => {
// eslint-disable-next-line jsx-a11y/anchor-is-validreturn ( key={idx}
title={item.name}
draggable
href="/#"
onDragStart={(ev) => onDrag(ev, item)}
>'iconfont ' + item.icon} style={{ fontSize: 13 }}>
);
})}
));
};
自定义配置项数据源, icon: 'icon-image' 指的是左侧显示的小图标。data数据name属性的值即代表image, topology通过此属性来判断渲染的是否是图片。image属性的值即是图片的地址。
{
group: '自定义图片',
children: [
{
name: 'image',
icon: 'icon-image',
data: {
text: '',
rect: {
width: 100,
height: 100
},
name: 'image',
image: require('./machine.jpg')
}
},
]
}
支持在线图片添加的功能 如果觉得使用本地图片麻烦, 我们可以换成在线的图片。
首先我们根据图片的url得出base64.
function getBase64(url, callback) {
var Img = new Image(),
dataURL = '';
Img.src = url + '?v=' + Math.random();
Img.setAttribute('crossOrigin', 'Anonymous');
Img.onload = function () {
var canvas = document.createElement('canvas'),
width = Img.width,
height = Img.height;
canvas.width = width;
canvas.height = height;
canvas.getContext('2d').drawImage(Img, 0, 0, width, height);
dataURL = canvas.toDataURL('image/jpeg');
return callback ? callback(dataURL) : null;
};
}
最后通过onDrag 方法将在线的图片拖到画布上即可。
const onDrag = (event, image) => {
event.dataTransfer.setData(
'Text',
JSON.stringify({
name: 'image',
rect: {
width: 100,
height: 100
},
image
})
);
};
支持新建文件, 打开文件, 导出json, 保存png与svg
新建一个空的画板
canvas.open({ nodes: [], lines: [] });
打开已有图形的文件
const onHandleImportJson = () => {
const input = document.createElement('input');
input.type = 'file';
input.onchange = event => {
const elem = event.srcElement || event.target;
if (elem.files && elem.files[0]) {
const reader = new FileReader();
reader.onload = e => {
const text = e.target.result + '';
try {
const data = JSON.parse(text);
canvas.open(data);
} catch (e) {
return false;
} finally {
}
};
reader.readAsText(elem.files[0]);
}
};
input.click();
}
将画好的图保存为json文件
import * as FileSaver from 'file-saver';
FileSaver.saveAs(
new Blob([JSON.stringify(canvas.data)], { type: 'text/plain;charset=utf-8' }),
`le5le.topology.json`
);
保存为png文件
canvas.saveAsImage('le5le.topology.png');
保存为SVG文件
const onHandleSaveToSvg = () => {
const C2S = window.C2S;
const ctx = new C2S(canvas.canvas.width + 200, canvas.canvas.height + 200);
if (canvas.data.pens) {
for (const item of canvas.data.pens) {
item.render(ctx);
}
}
let mySerializedSVG = ctx.getSerializedSvg();
mySerializedSVG = mySerializedSVG.replace(
'',
``
);
mySerializedSVG = mySerializedSVG.replace(/--le5le--/g, '');
const urlObject = window.URL || window;
const export_blob = new Blob([mySerializedSVG]);
const url = urlObject.createObjectURL(export_blob);
const a = document.createElement('a');
a.setAttribute('download', 'le5le.topology.svg');
a.setAttribute('href', url);
const evt = document.createEvent('MouseEvents');
evt.initEvent('click', true, true);
a.dispatchEvent(evt);
}
撤销、恢复、复制、剪切、粘贴
canvas.undo(); // 撤销
canvas.redo(); // 恢复
canvas.copy(); // 复制
canvas.cut(); // 剪切
canvas.paste(); // 粘贴
支持节点外观属性(位置大小边距, 边框样式, 字体样式)的设置
节点的位置和大小
const renderForm = useMemo(() => {
return "X(px)">
{getFieldDecorator('x', {
initialValue: x
})()}"Y(px)" name="y">
{getFieldDecorator('y', {
initialValue: y
})()}"宽(px)" name="width">
{getFieldDecorator('width', {
initialValue: width
})()}"高(px)" name="height">
{getFieldDecorator('height', {
initialValue: height
})()}"角度(deg)" name="rotate">
{getFieldDecorator('rotate', {
initialValue: rotate
})()}
}, [x, y, width, height, rotate, getFieldDecorator]);
边框样式
const renderStyleForm = useMemo(() => {
return "线条颜色">
{getFieldDecorator('strokeStyle', {
initialValue: strokeStyle
})(type="color" />)}"线条样式">
{getFieldDecorator('dash', {
initialValue: dash
})('95%' }}>_________---------_ _ _ _ _- . - . - .
)}"线条宽度">
{getFieldDecorator('lineWidth', {
initialValue: lineWidth
})('100%' }} />)}
}, [lineWidth, strokeStyle, dash, getFieldDecorator]);
字体设置
const renderFontForm = useMemo(() => {
return "字体颜色">
{getFieldDecorator('color', {
initialValue: color
})(type="color" />)}
"字体类型">
{getFieldDecorator('fontFamily', {
initialValue: fontFamily
})()}
"字体大小">
{getFieldDecorator('fontSize', {
initialValue: fontSize
})()}
"内容">
{getFieldDecorator('text', {
initialValue: text
})()}
}, [color, fontFamily, fontSize, text, getFieldDecorator])
当我们对表单里面的每一项都进行修改时, 都会调用onFormValueChange 方法去改变对应节点的属性。最后将修改后的节点, 更新到画布上。
if (changedValues.node) {
// 遍历查找修改的属性,赋值给原始Node
for (const key in changedValues.node) {
if (Array.isArray(changedValues.node[key])) {
} else if (typeof changedValues.node[key] === 'object') {
for (const k in changedValues.node[key]) {
selected.node[key][k] = changedValues.node[key][k];
}
} else {
selected.node[key] = changedValues.node[key];
}
}
}
canvas.updateProps(selected.node);
支持节点的数据属性 在实际的业务开发中, 难免会出现默认Node节点上的属性不够用的情况, 或者节点上有特定的业务数据。那么这个时候, 我们可以将这些特殊的数据存在节点的自定义数据字段。
const renderExtraDataForm = useMemo(() => {
return 1.单击事件 2.双击事件 3.websocket事件 4.mqtt"自定义数据字段">
{getFieldDecorator('data', {
initialValue: JSON.stringify(extraFields)
})()}
}, [extraFields, getFieldDecorator])
注意: Le5leTopology.Node节点默认是没有data这个属性的.
支持节点自定义事件的功能
由上图可知, 节点的事件分为事件类型和事件行为两部分。事件类型可以分为:
- 单击事件
- 双击事件
- websocket事件 4.mqtt事件。
事件行为可以分为:
- 跳转链接
- 执行动画
- 执行函数
- 执行window下的全局函数
- 更新属性数据。
Node节点中自定events属性, 因此根据文档中各个属性的枚举值, 我们可以很简单的绘制出各个事件类型与事件行为的对应关系。具体的代码由于篇幅限制, 就不粘贴了。有兴趣的同学可以阅读对应的源码.接下来, 我来演示一下如何对节点进行单击事件与websocket事件的绑定。
单击执行自定义函数
查看上图动画, 我们可以发现, 每次点击图形, 都会输出我是自定义函数。那么在我们的编辑器上, 该如何配置呢?
接收来自于websocket的值
我们通过websocket往服务器发送一个信号, 同时将会接收对应的值。首先我们需要事先连接好ws服务器。
canvas.openSocket('ws://123.207.136.134:9010/ajaxchattest');
这一步很关键, 否则之后的流程都将会报错。然后我们新增一个拥有点击事件的节点, 模拟信号的发起。
最后我们定义一个节点用于接收websocket返回的值。
接下来, 我们可以点击预览按钮, 测试我们配置的代码对不对。
支持线条的样式修改
目前只支持上图几种属性的设置。线条的更新与节点的更新类似, 我们直接修改线条的属性, 然后通过updateProps 更新对应线条的样式。
const onHandleLineFormValueChange = useCallback(
(value) => {
const { dash, lineWidth, strokeStyle, name, fromArrow, toArrow, ...other } = value;
const changedValues = {
line: { rect: other, lineWidth, dash, strokeStyle, name, fromArrow, toArrow }
};
if (changedValues.line) {
// 遍历查找修改的属性,赋值给原始line
for (const key in changedValues.line) {
if (Array.isArray(changedValues.line[key])) {
} else if (typeof changedValues.line[key] === 'object') {
for (const k in changedValues.line[key]) {
selected.line[key][k] = changedValues.line[key][k];
}
} else {
selected.line[key] = changedValues.line[key];
}
}
}
canvas.updateProps(selected.line);
},
[selected]
);
支持预览功能 当我们编辑完图形后, 需要预览。那么我们可以将画布上的数据通过路由传参(state)传递到新的页面, 最后通过
new Topology重新生成一块画布, 将图形渲染上去。
let reader = new FileReader();
const result = new Blob([JSON.stringify(canvas.data)], { type: 'text/plain;charset=utf-8' });
reader.readAsText(result, 'text/plain;charset=utf-8');
reader.onload = (e) => {
history.push({ pathname: '/preview', state: { data: JSON.parse(reader.result) } });
}
支持锁定,设置全局线的起始和终止箭头
锁定
canvas.lock(2)
canvas.lock(0)
设置默认的连线类型
const onHandleSelectMenu = data => {
setLineStyle(data.item.props.children);
canvas.data.lineName = data.key;
canvas.render();
}
设置默认的连线起始箭头
const onHandleSelectMenu1 = data => {
setFromArrowType(data.item.props.children);
canvas.data.fromArrowType = data.key;
canvas.render();
}
设置默认的连线终止箭头
const onHandleSelectMenu2 = data => {
setToArrowType(data.item.props.children);
canvas.data.toArrowType = data.key;
canvas.render();
}
支持自动排版功能 如果画出的图形比较乱, 那么可以使用自动居中的功能。首先我们通过 rect.calcCenter(); 获取当前图形的中心点, 然后我们计算出画布中心点与当前图形的中心点的差值, 最后通过调用 canvas.translate(x, y) 方法对图形进行平移。
const onHandleFit = () => {
const rect = canvas.getRect();
rect.calcCenter();
x = document.body.clientWidth / 2 - rect.center.x;
y = (document.body.clientHeight - 66) / 2 - rect.center.y;
canvas.translate(x, y);
};
结尾 虽然Topology的官网有各个API的详细说明, 但是从API转化到实际业务中, 还是需要耗费蛮多时间。其次官方的React版本的数据流比较复杂且对于新手上手的成本比较高, 因此就萌生了想要写一版简单的topology-react 帮助大家快速上手。最后的最后, 感谢Alsmile开源的绘图引擎。如果对你有帮助, 别忘了给一个小小的star哦, 谢谢啦~