在这篇文章中,我重点讲解画布实例的生成
生成div
首先需要有一个div框架,在我的组件中我设置了两个画布,一个是背景画布,一个是可操作画布,背景画布主要显示网格,这样做的好处就是既显示网格,又提供了精度
图一
html代码如下
html
<div id="bgCanvas" class="bg-canvas"></div>
<div
id="operateCanvas"
class="operate-canvas"
@keydown="spaceKeyDownEvents($event)"
@keyup="spaceKeyUpEvents($event)"
></div>
id为bgCanvas的就是背景画布,另一个就是操作画布,后面会介绍两个画布怎么进行同步,包括缩放和位移
创建实例
然后在ts中为这两个div注册画布
ts
//初始化画布实例
export const initCanvas = (): void => {
//设置背景画布
bgCanvas = new Graph({
container: document.getElementById('bgCanvas') as HTMLElement,
autoResize: true,
grid: {
type: 'doubleMesh',
size: 10,
visible: true,
args: [
{
color: '#e0e0e080',
thickness: 1
},
{
color: '#e0e0e0',
thickness: 1,
factor: 10
}
]
},
background: {
color: '#f7f7f7'
}
})
//设置画布
canvas = new Graph({
container: document.getElementById('operateCanvas') as HTMLElement,
autoResize: true,
//平移
panning: {
enabled: false,
eventTypes: ['leftMouseDown']
},
//缩放
mousewheel: {
enabled: true,
factor: 1.1
},
//网格
grid: {
size: 1,
visible: true
},
connecting: {
//是否可以连接到空白处
allowBlank(args: any) {
const shape = args.edge.shape
if (shape === 'ordinary-edge') {
return false
}
return true
},
//是否可以自己连自己
allowLoop(args: any) {
if (args.edge.shape === 'road-edge') {
return true
}
if (args.sourceCell.id !== args.targetCell.id) {
return true
} else {
return false
}
},
//是否允许连接到节点(非连接桩)
allowNode() {
return false
},
allowEdge() {
return true
},
//是否允许创建相同连线
allowMulti: 'withPort',
// 吸附半径
snap: {
radius: 15
},
//创建连接线
createEdge({ sourceCell }) {
if (sourceCell.shape === 'cross-node') {
return this.addEdge({
shape: 'road-edge'
})
} else {
return this.addEdge({
shape: 'ordinary-edge'
})
}
},
//点击连接桩时,先判断是否需要新增连接边
validateMagnet({ cell, magnet }) {
if (this.isNode(cell)) {
const node = cell as Node
const ports = node.getPorts()
//获取magnet中port属性
const portId = magnet.getAttribute('port')
const targetPort = ports.find((port) => port.id === portId)
const prohibitPorts = ['topIn', 'bottomIn', 'leftIn', 'rightIn']
if (prohibitPorts.includes(targetPort?.group || '')) {
return false
}
}
return true
},
//验证是否可以连接
validateConnection({ edge, targetMagnet }) {
if (edge?.shape === 'road-edge') {
return true
}
const prohibitPorts = ['topOut', 'bottomOut', 'leftOut', 'rightOut']
const targetPort = targetMagnet?.getAttribute('port-group')
if (prohibitPorts.includes(targetPort || '')) {
return false
}
return !!targetMagnet
}
},
interacting: (cellView: CellView) => {
const { isCanvasNodeInteractive, isEdgeMovable } = storeToRefs(useCanvasParameterStore())
if (!isCanvasNodeInteractive.value) {
return false
}
const { lockNodeIds } = storeToRefs(useLayerStore())
if (lockNodeIds.value.includes(cellView.cell.id)) {
return false
}
return {
edgeMovable: isEdgeMovable.value
}
}
})
//使用插件
//键盘快捷键
canvas.use(
new Keyboard({
enabled: true
})
)
//框选
canvas.use(
new Selection({
rubberband: true,
eventTypes: ['leftMouseDown'],
showNodeSelectionBox: true
})
)
//对齐线
canvas.use(
new Snapline({
enabled: false,
sharp: true,
tolerance: 5,
filter: (node: Node) => {
if (snaplineNodeShape.includes(node.shape)) {
return true
} else {
return false
}
}
})
)
//图形变换
canvas.use(
new Transform({
resizing: {
enabled: () => {
const { isGraphTransform } = useCanvasParameterStore()
return isGraphTransform
},
minWidth: 10,
minHeight: 0,
orthogonal: false,
restrict: false,
preserveAspectRatio: false
}
})
)
dnd = new Dnd({
//canvas是画布组件实例
target: canvas,
//拖拽结束时的回调函数
validateNode(droppingNode: Node): boolean {
//给节点设置连接桩
setNodePort(droppingNode)
//渲染进程给主进程发送消息,添加节点
renderAddNode(droppingNode)
//返回false,这里不添加节点
return false
}
})
}
initCanvas方法可以在onMounted钩子函数里面直接调用即可
到这一步,我们的画布就显示出来了,但是我们创建的画布缺少交互,就比如上面提到的,背景画布和操作画布怎么进行同步,那下面我们就给我们的画布实例添加"五感"
创建监听事件
canvas是操作画布,bgCanvas是背景画布
ts
//画布监听事件
export const initCanvasEvents = (): void => {
//画布-缩放事件
canvas.on('scale', () => {
//获取canvas的缩放级别
const zoom = canvas.zoom()
bgCanvas.zoom(zoom, { absolute: true })
const { canvasScale } = storeToRefs(useCanvasParameterStore())
canvasScale.value = zoom
//给画布操作发消息
useMessageStore().addCanvasOperate({
source: '画布缩放',
content: `当前缩放比例`,
otherMsg: zoom.toFixed(2)
})
prohibitCanvasMoveMsg = false
})
//画布-移动事件
canvas.on('translate', ({ tx, ty }: { tx: number; ty: number }) => {
const { canvasPosition } = storeToRefs(useCanvasParameterStore())
canvasPosition.value.x = -tx
canvasPosition.value.y = -ty
bgCanvas.translate(tx, ty)
if (prohibitCanvasMoveMsg) {
//给画布操作发消息
useMessageStore().addCanvasOperate({
source: '画布移动',
content: `当前画布位置`,
otherMsg: `x:${-tx.toFixed(2)},y:${-ty.toFixed(2)}`
})
} else {
prohibitCanvasMoveMsg = true
}
})
}
在上面代码中,我们可以看到,操作画布监听了两个事件
- 缩放事件
- 位移事件
当我们收到这两个事件后,我们会立即同步到背景画布中,当然initCanvasEvents方法也需要在onMounted钩子函数里面直接调用,那类似于这种的事件还有很多,如图二图三所示
图二
图三
到此我们的画布实例就彻底完成,接下来就是美化我们创建的节点
创建Cell
这一步主要是为了美化我们创建的节点,对于美术功底不太好的同学来说可能会比较难,下面先展示效果,如图四,各个节点在画布中的效果
图四
接下来是创建这些节点的源代码
ts
//注册节点与连接线
export const registerNodeAndEdge = (): void => {
//注册普通节点,长100,宽100
register({
shape: 'ordinary-node',
width: 100,
height: 100,
component: OrdinaryNode,
ports: {
groups: customPorts
}
})
//创建产品节点
register({
shape: 'product-node',
width: 20,
height: 20,
component: ProductNode
})
//注册十字路节点
register({
shape: 'cross-node',
width: 120,
height: 120,
component: Crossing,
ports: {
groups: customPorts
}
})
//创建搬运流节点
register({
shape: 'transport-node',
width: 100,
height: 100,
component: TransportNode
})
//注册添加事件的节点
register({
shape: 'add-node',
width: 18,
height: 18,
component: AddNode,
attrs: {
body: {
magnet: true
}
}
})
//自定义顶点区域
Graph.registerNode(
'custom-port-node',
{
inherit: 'polygon',
width: 100,
height: 100,
attrs: {
body: {
fill: '#d6ddff66',
stroke: 'transparent',
points: '0,0 100,0 100,100 0,100',
event: 'customPortNodeClick'
}
}
},
true
)
//注册编辑多边形的顶点小圆圈
Graph.registerNode(
'free-edit-circle',
{
inherit: 'circle',
width: 4,
height: 4,
attrs: {
body: {
fill: '#fff',
stroke: '#0087dc',
strokeWidth: 1
}
}
},
true
)
//注册编辑多边形的边
Graph.registerEdge(
'free-edit-edge',
{
inherit: 'edge',
markup: [
{
tagName: 'path',
selector: 'p1'
},
{
tagName: 'path',
groupSelector: 'arrow',
selector: 'a1'
}
],
attrs: {
p1: {
stroke: '#0087dc',
strokeWidth: 0.7,
connection: true,
fill: 'none'
},
a1: {
d: 'M -2 0 0 -2 2 0 0 2 z',
fill: '#fff',
stroke: '#ED8A19',
atConnectionRatio: 0.5,
cursor: 'pointer',
event: 'freeEditEdgeClick'
}
}
},
true
)
//注册纯文本节点
Graph.registerNode(
'text-node',
{
inherit: 'rect',
width: 100,
height: 25,
markup: [
{
tagName: 'rect',
selector: 'body'
},
{
tagName: 'text',
selector: 'label'
}
],
attrs: {
rect: {
fill: 'transparent',
stroke: 'transparent'
},
label: {
text: '自定义文字',
fill: '#4E5969',
fontSize: 15,
fontWeight: 'regular',
letterSpacing: 0,
event: 'textNodeClick'
}
}
},
true
)
//注册普通连接线
Graph.registerEdge(
'ordinary-edge',
{
inherit: 'edge',
anchor: 'center',
connectionPoint: 'anchor',
markup: [
{
tagName: 'path',
selector: 'p1'
},
{
tagName: 'path',
groupSelector: 'arrow',
selector: 'a1'
}
],
attrs: {
p1: {
connection: true,
fill: 'none',
stroke: '#A2B1C3',
strokeWidth: 2,
strokeLinejoin: 'round'
},
a1: {
d: 'M 0 -6 8 0 0 6 z',
fill: '#ED8A19',
stroke: '#ED8A19',
atConnectionRatio: 0.5,
cursor: 'pointer'
}
}
},
true
)
//注册传送带鼠标拖下来的样式节点
Graph.registerNode(
'edge-drag-node',
{
inherit: 'rect',
width: 150,
height: 20,
markup: [
{
tagName: 'rect',
selector: 'body'
},
{
tagName: 'text',
selector: 'label'
}
],
attrs: {
rect: {
fill: 'transparent',
stroke: '#c0c0c0',
strokeWidth: 1
},
label: {
text: '',
fill: '#4E5969',
fontSize: 13,
textAnchor: 'middle'
}
}
},
true
)
//注册传送带
Graph.registerEdge(
'conveyor-belt',
{
inherit: 'edge',
router: {
name: 'orth'
},
connector: { name: 'rounded' },
anchor: 'center',
connectionPoint: 'anchor',
markup: [
{
tagName: 'rect',
selector: 'port'
},
{
tagName: 'path',
selector: 'p1'
},
{
tagName: 'path',
selector: 'p2'
},
{
tagName: 'path',
selector: 'p3'
},
{
tagName: 'text',
selector: 'describe'
}
],
attrs: {
port: {
y: -7,
connection: true,
width: 5,
height: 15,
fill: '#c0c0c0',
atConnectionRatio: 0,
magnet: true
},
p1: {
connection: true,
fill: 'none',
stroke: '#c0c0c0',
strokeWidth: 20,
strokeLinejoin: 'round',
event: 'conveyorClick'
},
p2: {
connection: true,
fill: 'none',
stroke: '#f5f8ff',
strokeWidth: 17,
strokeLinejoin: 'round',
event: 'conveyorClick'
},
p3: {
connection: true,
fill: 'none',
stroke: '#c0c0c0',
strokeWidth: 20,
pointerEvents: 'none',
strokeLinejoin: 'round',
strokeDasharray: '1,15',
event: 'conveyorClick'
},
describe: {
atConnectionRatio: 0.5,
textAnchor: 'middle',
textVerticalAnchor: 'middle',
fontSize: 13,
fill: '#0087dc',
text: '➤'
}
}
},
true
)
//注册道路连线
Graph.registerEdge(
'road-edge',
{
inherit: 'edge',
router: {
name: 'orth'
},
connector: { name: 'rounded' },
anchor: 'center',
connectionPoint: 'anchor',
markup: [
{
tagName: 'path',
selector: 'p1'
},
{
tagName: 'path',
selector: 'p2'
}
],
attrs: {
p1: {
connection: true,
fill: 'none',
stroke: '#A2B1C3',
strokeWidth: 20,
strokeLinejoin: 'round',
event: 'roadEdgeClick'
},
p2: {
connection: true,
fill: 'none',
stroke: '#d9c956',
strokeWidth: 3,
pointerEvents: 'none',
strokeLinejoin: 'round',
strokeDasharray: 5,
event: 'roadEdgeClick'
}
}
},
true
)
//注册警戒线
Graph.registerEdge(
'warning-line',
{
inherit: 'edge',
router: {
name: 'orth'
},
connector: { name: 'rounded' },
anchor: 'center',
connectionPoint: 'anchor',
markup: [
{
tagName: 'path',
selector: 'p1'
},
{
tagName: 'path',
selector: 'p2'
},
{
tagName: 'path',
selector: 'p3'
}
],
attrs: {
p1: {
connection: true,
fill: 'none',
stroke: '#f5cf36ff',
strokeWidth: 12,
strokeLinejoin: 'round'
},
p2: {
connection: true,
fill: 'none',
stroke: '#ffd83b',
strokeWidth: 8,
strokeDasharray: '10,10',
strokeDashoffset: 0,
strokeLinejoin: 'round'
},
p3: {
connection: true,
fill: 'none',
stroke: '#3f4347',
strokeWidth: 8,
strokeDasharray: '10,10',
strokeDashoffset: 10,
strokeLinejoin: 'round'
}
}
},
true
)
}
然后我把官网的一些示例推荐给大家,方便大家的理解
到此,我们的画布就正式的创建完毕,谢谢大家,在下一篇文章中,我会给大家介绍一个重要的组件,【标尺-刻度尺】,也是完全自主研发