自定义渲染器:打造独特的视觉风格

通过自定义渲染器,可以完全改变BPMN元素的视觉呈现,实现品牌化、主题化的流程图。本文详细讲解如何编写自定义Renderer,绘制SVG图形,并优化渲染性能。

渲染器的工作原理

diagram-js使用Renderer(渲染器)将BPMN元素绘制为SVG图形:

1
2
3
元素定义 → Renderer → SVG DOM → 浏览器显示
↓ ↓
businessObject drawShape/drawConnection

默认渲染器:bpmn-js提供标准BPMN符号
自定义渲染器:替换或扩展默认样式

基础自定义渲染器

最简单的自定义渲染

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
class CustomRenderer extends BaseRenderer {
constructor(eventBus, styles) {
super(eventBus, 2000); // 优先级:2000表示高于默认渲染器
this.styles = styles;
}

canRender(element) {
// 决定哪些元素由此渲染器处理
return element.type === 'bpmn:UserTask';
}

drawShape(parentNode, element) {
// 绘制形状
const rect = drawRect(parentNode, element.width, element.height, {
fill: '#e1f5fe',
stroke: '#01579b',
strokeWidth: 2,
r: 5 // 圆角
});

// 添加图标
const icon = drawImage(parentNode, {
href: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiPjxwYXRoIGQ9Ik0xMiwxMkw4LDhIMTZMMTIsMTJaIi8+PC9zdmc+',
x: element.width / 2 - 12,
y: element.height / 2 - 12,
width: 24,
height: 24
});

return rect;
}

getShapePath(element) {
// 定义元素的边界路径(用于连接点计算)
const x = element.x;
const y = element.y;
const width = element.width;
const height = element.height;

return [
['M', x, y],
['l', width, 0],
['l', 0, height],
['l', -width, 0],
['z']
];
}
}

CustomRenderer.$inject = ['eventBus', 'styles'];
inherits(CustomRenderer, BaseRenderer);

// 辅助函数
function drawRect(parentNode, width, height, attrs) {
const rect = svgCreate('rect');
svgAttr(rect, {
width: width,
height: height,
rx: attrs.r,
ry: attrs.r
});
svgAttr(rect, attrs);
svgAppend(parentNode, rect);
return rect;
}

function drawImage(parentNode, attrs) {
const image = svgCreate('image');
svgAttr(image, attrs);
svgAppend(parentNode, image);
return image;
}

集成到Modeler

1
2
3
4
5
6
7
8
9
10
11
import CustomRenderer from './CustomRenderer';

const modeler = new BpmnJS({
container: '#canvas',
additionalModules: [
{
__init__: ['customRenderer'],
customRenderer: ['type', CustomRenderer]
}
]
});

高级渲染技巧

根据属性改变样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
class AttributeBasedRenderer extends BaseRenderer {
constructor(eventBus, styles) {
super(eventBus, 2000);
this.styles = styles;
}

canRender(element) {
return element.type === 'bpmn:UserTask' ||
element.type === 'bpmn:ServiceTask';
}

drawShape(parentNode, element) {
const bo = element.businessObject;
const priority = bo.get('custom:priority');

// 根据优先级选择颜色
const colorScheme = this.getColorScheme(priority);

// 绘制基础矩形
const rect = drawRect(parentNode, element.width, element.height, {
fill: colorScheme.fill,
stroke: colorScheme.stroke,
strokeWidth: 3,
r: 8
});

// 添加优先级指示器
if (priority === 'high') {
this.drawPriorityBadge(parentNode, element);
}

// 添加任务图标
this.drawTaskIcon(parentNode, element);

// 添加状态指示
const status = bo.get('custom:status');
if (status) {
this.drawStatusIndicator(parentNode, element, status);
}

return rect;
}

getColorScheme(priority) {
const schemes = {
'high': {
fill: '#fff1f0',
stroke: '#cf1322',
badge: '#ff4d4f'
},
'medium': {
fill: '#fffbe6',
stroke: '#d48806',
badge: '#faad14'
},
'low': {
fill: '#f6ffed',
stroke: '#389e0d',
badge: '#52c41a'
}
};

return schemes[priority] || {
fill: '#fafafa',
stroke: '#d9d9d9',
badge: '#8c8c8c'
};
}

drawPriorityBadge(parentNode, element) {
const badge = svgCreate('circle');
svgAttr(badge, {
cx: element.width - 10,
cy: 10,
r: 6,
fill: '#ff4d4f',
stroke: 'white',
strokeWidth: 2
});
svgAppend(parentNode, badge);

const text = svgCreate('text');
svgAttr(text, {
x: element.width - 10,
y: 10,
fill: 'white',
fontSize: 10,
fontWeight: 'bold',
textAnchor: 'middle',
dominantBaseline: 'middle'
});
text.textContent = '!';
svgAppend(parentNode, text);
}

drawTaskIcon(parentNode, element) {
const iconType = element.type === 'bpmn:UserTask' ? 'user' : 'service';
const icon = svgCreate('text');

svgAttr(icon, {
x: 10,
y: element.height / 2,
fontSize: 20,
dominantBaseline: 'middle'
});

icon.textContent = iconType === 'user' ? '👤' : '⚙️';
svgAppend(parentNode, icon);
}

drawStatusIndicator(parentNode, element, status) {
const statusColors = {
'completed': '#52c41a',
'active': '#1890ff',
'pending': '#faad14',
'error': '#ff4d4f'
};

const indicator = svgCreate('rect');
svgAttr(indicator, {
x: 0,
y: 0,
width: 4,
height: element.height,
fill: statusColors[status] || '#d9d9d9'
});
svgAppend(parentNode, indicator);
}
}

AttributeBasedRenderer.$inject = ['eventBus', 'styles'];
inherits(AttributeBasedRenderer, BaseRenderer);

渐变与阴影效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
class ModernRenderer extends BaseRenderer {
constructor(eventBus, styles, canvas) {
super(eventBus, 2000);
this.styles = styles;
this.canvas = canvas;
this.initDefs();
}

initDefs() {
// 创建SVG定义区域
const svg = this.canvas._svg;
let defs = svg.querySelector('defs');

if (!defs) {
defs = svgCreate('defs');
svgPrepend(svg, defs);
}

// 定义渐变
const gradient = svgCreate('linearGradient');
svgAttr(gradient, {
id: 'taskGradient',
x1: '0%',
y1: '0%',
x2: '0%',
y2: '100%'
});

const stop1 = svgCreate('stop');
svgAttr(stop1, { offset: '0%', stopColor: '#667eea', stopOpacity: 1 });
const stop2 = svgCreate('stop');
svgAttr(stop2, { offset: '100%', stopColor: '#764ba2', stopOpacity: 1 });

svgAppend(gradient, stop1);
svgAppend(gradient, stop2);
svgAppend(defs, gradient);

// 定义阴影滤镜
const filter = svgCreate('filter');
svgAttr(filter, {
id: 'dropShadow',
x: '-50%',
y: '-50%',
width: '200%',
height: '200%'
});

const feGaussianBlur = svgCreate('feGaussianBlur');
svgAttr(feGaussianBlur, {
in: 'SourceAlpha',
stdDeviation: 3
});

const feOffset = svgCreate('feOffset');
svgAttr(feOffset, {
dx: 0,
dy: 2,
result: 'offsetblur'
});

const feMerge = svgCreate('feMerge');
const feMergeNode1 = svgCreate('feMergeNode');
const feMergeNode2 = svgCreate('feMergeNode');
svgAttr(feMergeNode2, { in: 'SourceGraphic' });

svgAppend(feMerge, feMergeNode1);
svgAppend(feMerge, feMergeNode2);

svgAppend(filter, feGaussianBlur);
svgAppend(filter, feOffset);
svgAppend(filter, feMerge);
svgAppend(defs, filter);
}

canRender(element) {
return element.type === 'bpmn:UserTask';
}

drawShape(parentNode, element) {
const rect = drawRect(parentNode, element.width, element.height, {
fill: 'url(#taskGradient)',
stroke: 'none',
r: 10,
filter: 'url(#dropShadow)'
});

// 添加玻璃效果
const highlight = svgCreate('rect');
svgAttr(highlight, {
x: 0,
y: 0,
width: element.width,
height: element.height / 2,
rx: 10,
ry: 10,
fill: 'white',
opacity: 0.2
});
svgAppend(parentNode, highlight);

return rect;
}
}

ModernRenderer.$inject = ['eventBus', 'styles', 'canvas'];
inherits(ModernRenderer, BaseRenderer);

动画效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class AnimatedRenderer extends BaseRenderer {
constructor(eventBus, styles) {
super(eventBus, 2000);
this.styles = styles;
}

canRender(element) {
return element.type === 'bpmn:ServiceTask';
}

drawShape(parentNode, element) {
const bo = element.businessObject;
const isActive = bo.get('custom:isActive');

// 绘制基础形状
const rect = drawRect(parentNode, element.width, element.height, {
fill: '#e6f7ff',
stroke: '#1890ff',
strokeWidth: 2,
r: 8
});

// 如果任务正在执行,添加动画
if (isActive) {
this.addPulseAnimation(rect);
this.addSpinner(parentNode, element);
}

return rect;
}

addPulseAnimation(element) {
const animate = svgCreate('animate');
svgAttr(animate, {
attributeName: 'stroke-width',
values: '2;4;2',
dur: '1.5s',
repeatCount: 'indefinite'
});
svgAppend(element, animate);

const animateOpacity = svgCreate('animate');
svgAttr(animateOpacity, {
attributeName: 'opacity',
values: '1;0.6;1',
dur: '1.5s',
repeatCount: 'indefinite'
});
svgAppend(element, animateOpacity);
}

addSpinner(parentNode, element) {
const spinnerGroup = svgCreate('g');
svgAttr(spinnerGroup, {
transform: `translate(${element.width - 20}, 10)`
});

const circle = svgCreate('circle');
svgAttr(circle, {
cx: 0,
cy: 0,
r: 8,
fill: 'none',
stroke: '#1890ff',
strokeWidth: 2,
strokeDasharray: '15, 20'
});

const animateRotate = svgCreate('animateTransform');
svgAttr(animateRotate, {
attributeName: 'transform',
type: 'rotate',
from: '0 0 0',
to: '360 0 0',
dur: '1s',
repeatCount: 'indefinite'
});

svgAppend(circle, animateRotate);
svgAppend(spinnerGroup, circle);
svgAppend(parentNode, spinnerGroup);
}
}

AnimatedRenderer.$inject = ['eventBus', 'styles'];
inherits(AnimatedRenderer, BaseRenderer);

性能优化

渲染缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
class OptimizedRenderer extends BaseRenderer {
constructor(eventBus, styles) {
super(eventBus, 2000);
this.styles = styles;
this.cache = new Map();
}

canRender(element) {
return element.type === 'bpmn:UserTask';
}

drawShape(parentNode, element) {
const cacheKey = this.getCacheKey(element);

// 检查缓存
if (this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
return cached.cloneNode(true);
}

// 渲染新元素
const shape = this.renderShape(parentNode, element);

// 存入缓存
this.cache.set(cacheKey, shape.cloneNode(true));

return shape;
}

getCacheKey(element) {
const bo = element.businessObject;
return `${element.type}_${bo.get('custom:priority')}_${bo.get('custom:status')}`;
}

renderShape(parentNode, element) {
// 实际渲染逻辑
const rect = drawRect(parentNode, element.width, element.height, {
fill: '#e1f5fe',
stroke: '#01579b',
strokeWidth: 2,
r: 5
});
return rect;
}
}

OptimizedRenderer.$inject = ['eventBus', 'styles'];
inherits(OptimizedRenderer, BaseRenderer);

延迟渲染细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class LazyDetailRenderer extends BaseRenderer {
constructor(eventBus, styles, canvas) {
super(eventBus, 2000);
this.styles = styles;
this.canvas = canvas;
}

canRender(element) {
return element.type === 'bpmn:UserTask';
}

drawShape(parentNode, element) {
// 始终绘制基础形状
const rect = this.drawBaseShape(parentNode, element);

// 只在放大时显示细节
const zoom = this.canvas.zoom();
if (zoom >= 0.8) {
this.drawDetailedContent(parentNode, element);
}

return rect;
}

drawBaseShape(parentNode, element) {
return drawRect(parentNode, element.width, element.height, {
fill: '#e1f5fe',
stroke: '#01579b',
strokeWidth: 2,
r: 5
});
}

drawDetailedContent(parentNode, element) {
// 添加图标、文本等细节
const icon = drawImage(parentNode, {
href: 'icons/user.svg',
x: 10,
y: 10,
width: 20,
height: 20
});

const text = svgCreate('text');
svgAttr(text, {
x: 40,
y: element.height / 2,
fontSize: 12,
dominantBaseline: 'middle'
});
text.textContent = element.businessObject.name || 'Unnamed Task';
svgAppend(parentNode, text);
}
}

LazyDetailRenderer.$inject = ['eventBus', 'styles', 'canvas'];
inherits(LazyDetailRenderer, BaseRenderer);

实战案例:品牌化渲染器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
class BrandedRenderer extends BaseRenderer {
constructor(eventBus, styles, textRenderer) {
super(eventBus, 2000);
this.styles = styles;
this.textRenderer = textRenderer;

// 品牌配色
this.brandColors = {
primary: '#6366f1',
secondary: '#8b5cf6',
success: '#10b981',
warning: '#f59e0b',
danger: '#ef4444',
light: '#f3f4f6',
dark: '#1f2937'
};
}

canRender(element) {
return element.type.startsWith('bpmn:');
}

drawShape(parentNode, element) {
switch(element.type) {
case 'bpmn:StartEvent':
return this.drawStartEvent(parentNode, element);
case 'bpmn:EndEvent':
return this.drawEndEvent(parentNode, element);
case 'bpmn:UserTask':
return this.drawUserTask(parentNode, element);
case 'bpmn:ServiceTask':
return this.drawServiceTask(parentNode, element);
case 'bpmn:ExclusiveGateway':
return this.drawGateway(parentNode, element);
default:
return null; // 使用默认渲染器
}
}

drawStartEvent(parentNode, element) {
const circle = svgCreate('circle');
svgAttr(circle, {
cx: element.width / 2,
cy: element.height / 2,
r: element.width / 2,
fill: this.brandColors.success,
stroke: this.brandColors.dark,
strokeWidth: 2
});
svgAppend(parentNode, circle);

// 添加播放图标
const playIcon = svgCreate('polygon');
svgAttr(playIcon, {
points: '12,8 12,20 22,14',
fill: 'white'
});
svgAppend(parentNode, playIcon);

return circle;
}

drawEndEvent(parentNode, element) {
const circle = svgCreate('circle');
svgAttr(circle, {
cx: element.width / 2,
cy: element.height / 2,
r: element.width / 2,
fill: this.brandColors.danger,
stroke: this.brandColors.dark,
strokeWidth: 3
});
svgAppend(parentNode, circle);

// 添加停止图标
const stopIcon = svgCreate('rect');
svgAttr(stopIcon, {
x: 10,
y: 10,
width: 16,
height: 16,
fill: 'white',
rx: 2
});
svgAppend(parentNode, stopIcon);

return circle;
}

drawUserTask(parentNode, element) {
const group = svgCreate('g');

// 圆角矩形背景
const rect = svgCreate('rect');
svgAttr(rect, {
width: element.width,
height: element.height,
rx: 12,
ry: 12,
fill: this.brandColors.primary,
stroke: this.brandColors.dark,
strokeWidth: 2
});
svgAppend(group, rect);

// 用户图标
const userIcon = svgCreate('text');
svgAttr(userIcon, {
x: 15,
y: element.height / 2 + 8,
fontSize: 24,
fill: 'white'
});
userIcon.textContent = '👤';
svgAppend(group, userIcon);

// 任务名称
const text = svgCreate('text');
svgAttr(text, {
x: 50,
y: element.height / 2,
fontSize: 14,
fill: 'white',
fontWeight: 'bold',
dominantBaseline: 'middle'
});
text.textContent = element.businessObject.name || 'User Task';
svgAppend(group, text);

svgAppend(parentNode, group);
return group;
}

drawServiceTask(parentNode, element) {
const group = svgCreate('g');

const rect = svgCreate('rect');
svgAttr(rect, {
width: element.width,
height: element.height,
rx: 12,
ry: 12,
fill: this.brandColors.secondary,
stroke: this.brandColors.dark,
strokeWidth: 2
});
svgAppend(group, rect);

const serviceIcon = svgCreate('text');
svgAttr(serviceIcon, {
x: 15,
y: element.height / 2 + 8,
fontSize: 24,
fill: 'white'
});
serviceIcon.textContent = '⚙️';
svgAppend(group, serviceIcon);

svgAppend(parentNode, group);
return group;
}

drawGateway(parentNode, element) {
const diamond = svgCreate('polygon');
const halfWidth = element.width / 2;
const halfHeight = element.height / 2;

svgAttr(diamond, {
points: `${halfWidth},0 ${element.width},${halfHeight} ${halfWidth},${element.height} 0,${halfHeight}`,
fill: this.brandColors.warning,
stroke: this.brandColors.dark,
strokeWidth: 2
});
svgAppend(parentNode, diamond);

// X标记
const path = svgCreate('path');
svgAttr(path, {
d: `M ${halfWidth - 10},${halfHeight - 10} L ${halfWidth + 10},${halfHeight + 10} M ${halfWidth + 10},${halfHeight - 10} L ${halfWidth - 10},${halfHeight + 10}`,
stroke: 'white',
strokeWidth: 3,
strokeLinecap: 'round'
});
svgAppend(parentNode, path);

return diamond;
}
}

BrandedRenderer.$inject = ['eventBus', 'styles', 'textRenderer'];
inherits(BrandedRenderer, BaseRenderer);

小结

掌握自定义渲染器:

  • ✓ 理解渲染器的工作原理
  • ✓ 绘制基础SVG图形
  • ✓ 根据属性动态渲染
  • ✓ 添加渐变、阴影、动画效果
  • ✓ 优化渲染性能

便能构建独特、美观的流程图视觉呈现

参考资料