画布拓展内容(#canvas)
#canvas 用于在图谱画布坐标系中放置自定义内容。它适合画“属于图谱世界”的东西,例如分组框、泳道、网格、标尺、业务区域、坐标注释。这些内容会和节点、连线一样跟随画布平移与缩放。
如果你要放的是固定在视口上的工具栏、按钮、菜单、属性面板,请使用 #view,不要使用 #canvas。
1. 各平台写法
| 平台 | 写法 | 说明 |
|---|---|---|
| Vue 3 / Vue 2 | <template #canvas>...</template> |
推荐写法 |
| Vue 3 / Vue 2 | 默认插槽 | 当前实现也会把默认插槽放到 canvas 底层 |
| React | <RGSlotOnCanvas>...</RGSlotOnCanvas> |
推荐写法 |
| React | 普通 children | 当前实现会把未识别为其他 slot 的 children 放到 canvas 底层 |
| Svelte | <div slot="canvas">...</div> |
推荐写法 |
| Svelte | 默认插槽 | 当前实现也会把默认插槽放到 canvas 底层 |
2. 渲染位置与层级
当前实现中,#canvas 被渲染到:
<div class="rg-map-canvas rg-canvas-behind">
<div class="rg-canvas-slot rg-canvas-slot-behind">
<!-- #canvas / 默认子内容 -->
</div>
</div>
它位于节点和连线下方。简化层级:
| 层级 | 内容 | 是否跟随画布缩放/平移 | 说明 |
|---|---|---|---|
| 背景层 | #background |
否 | 视口固定背景 |
| 画布底层 | #canvas |
是 | 节点/连线下方 |
| 主图谱层 | 节点、连线、节点/连线插槽 | 是 | 图谱主体 |
| 画布前景 | #canvas-above |
是 | 节点/连线上方 |
| 视口层 | #view |
否 | 固定 UI |
#canvas 与主图谱层使用同一套画布变换:
transform: translate(canvasOffset.x, canvasOffset.y) scale(canvasZoom / 100);
transform-origin: 0 0;
因此 #canvas 内部元素的 left/top、SVG x/y、Canvas 绘制坐标都应该使用画布坐标,不是浏览器客户端坐标,也不是 RelationGraph 视口坐标。
3. 坐标换算
常用 API:
| API | 用途 |
|---|---|
graphInstance.getCanvasXyByClientXy({ x, y }) |
将鼠标客户端坐标转换为画布坐标 |
graphInstance.getCanvasXyByViewXy({ x, y }) |
将 RelationGraph 视口坐标转换为画布坐标 |
graphInstance.getViewXyByCanvasXy({ x, y }) |
将画布坐标转换为视口坐标 |
graphInstance.getViewXyByEvent(event) |
从鼠标/触摸事件直接得到视口坐标 |
示例:点击按钮后在当前鼠标位置生成一个画布注释:
function createMarker(event: MouseEvent) {
const point = graphInstance.getCanvasXyByClientXy({
x: event.clientX,
y: event.clientY
});
markers.value.push({
id: `marker-${Date.now()}`,
x: point.x,
y: point.y,
text: '新标记'
});
}
渲染时直接使用画布坐标:
<template #canvas>
<div
v-for="marker in markers"
:key="marker.id"
class="canvas-marker"
:style="{
left: marker.x + 'px',
top: marker.y + 'px'
}"
>
{{ marker.text }}
</div>
</template>
4. 事件处理
画布层外壳 .rg-map-canvas 默认是 pointer-events: none,这是为了不阻断画布拖拽、框选、节点/连线事件。你放到 #canvas 里的交互元素如果需要接收点击,必须显式开启事件。
推荐使用内置 class:
<button class="rg-events-all">操作</button>
或直接写:
<button style="pointer-events: auto;">操作</button>
Vue 示例:
<template #canvas>
<button
class="marker-button rg-events-all"
:style="{ left: marker.x + 'px', top: marker.y + 'px' }"
@mousedown.stop
@click.stop="openMarker(marker)"
>
{{ marker.text }}
</button>
</template>
注意:
- 只给需要交互的元素开启事件,不要把整个大面积覆盖层都设为
pointer-events: auto,否则可能挡住画布拖拽、节点拖拽和线条点击。 - 如果交互元素位于节点下方但被节点覆盖,使用
#canvas-above更合适。 - 如果交互元素应该固定在屏幕上,使用
#view更合适。
5. 分组框示例
分组框通常属于画布坐标系,应放在 #canvas。它会跟随图谱移动和缩放,并显示在节点/连线下方。
<script setup lang="ts">
const groups = [
{
id: 'group-a',
title: '研发团队',
x: -260,
y: -140,
width: 420,
height: 260,
color: 'rgba(59, 130, 246, 0.08)',
borderColor: '#3b82f6'
},
{
id: 'group-b',
title: '业务团队',
x: 220,
y: -120,
width: 380,
height: 240,
color: 'rgba(16, 185, 129, 0.08)',
borderColor: '#10b981'
}
];
</script>
<template>
<RelationGraph :options="graphOptions" :initial-data="graphData">
<template #canvas>
<div
v-for="group in groups"
:key="group.id"
class="group-box"
:style="{
left: group.x + 'px',
top: group.y + 'px',
width: group.width + 'px',
height: group.height + 'px',
background: group.color,
borderColor: group.borderColor
}"
>
<div class="group-title">{{ group.title }}</div>
</div>
</template>
</RelationGraph>
</template>
.group-box {
position: absolute;
box-sizing: border-box;
border: 1px dashed;
border-radius: 8px;
pointer-events: none;
}
.group-title {
position: absolute;
left: 10px;
top: 8px;
font-size: 12px;
color: #374151;
}
6. SVG 辅助层示例
如果你需要画坐标线、区域轮廓、辅助路径,SVG 往往比大量 DOM 更合适。
<template #canvas>
<svg
class="canvas-svg-layer"
:style="{
left: '-1000px',
top: '-1000px',
width: '2000px',
height: '2000px'
}"
viewBox="-1000 -1000 2000 2000"
>
<line x1="-1000" y1="0" x2="1000" y2="0" class="axis-line" />
<line x1="0" y1="-1000" x2="0" y2="1000" class="axis-line" />
<rect
v-for="area in areas"
:key="area.id"
:x="area.x"
:y="area.y"
:width="area.width"
:height="area.height"
class="area-rect"
/>
</svg>
</template>
.canvas-svg-layer {
position: absolute;
overflow: visible;
pointer-events: none;
}
.axis-line {
stroke: rgba(100, 116, 139, 0.25);
stroke-width: 1;
}
.area-rect {
fill: rgba(245, 158, 11, 0.08);
stroke: rgba(245, 158, 11, 0.8);
stroke-dasharray: 6 4;
}
7. 与 #canvas-above 的区别
| 对比项 | #canvas |
#canvas-above |
|---|---|---|
| 层级 | 节点/连线下方 | 节点/连线上方 |
| 坐标系 | 画布坐标 | 画布坐标 |
| 是否跟随缩放/平移 | 是 | 是 |
| 典型用途 | 分组框、泳道、网格、底层注释 | 选区、拖拽提示、节点上方临时浮标 |
| 是否容易遮挡节点 | 不会遮挡节点,但可能被节点挡住 | 可能遮挡节点,需要控制事件和透明区域 |
选择建议:
- 属于背景语义的图元放
#canvas。 - 需要显示在节点/连线上方的提示放
#canvas-above。 - 固定在屏幕上的工具 UI 放
#view。
8. 与假线目标、连接目标的关系
如果你在 #canvas 中放置可连接的自定义目标,可以配合 RGConnectTarget 或假线目标 API。假线目标支持多种类型,详见 假线数据模型。
常见场景:
| 场景 | 推荐做法 |
|---|---|
| 拖拽到画布某点生成线 | 使用 CanvasPoint 类型目标或自己在鼠标事件中换算画布坐标 |
| 拖拽到自定义 HTML 元素 | 使用 HTMLElementId,确保元素有稳定 id |
| 拖拽到节点内某个小点位 | 使用 NodePoint 或在节点插槽里放连接控制器 |
| 画布上有业务端口 | 放在 #canvas 或 #canvas-above,并明确开启 pointer-events |
9. 性能建议
#canvas 内容会跟随画布缩放。大量 DOM、阴影、滤镜、复杂图片可能影响平移和缩放性能。
建议:
- 大量静态背景元素优先合并为一层 SVG 或 Canvas。
- 频繁变化的辅助内容尽量减少 DOM 数量。
- 大面积覆盖层默认
pointer-events: none。 - 避免在每个节点移动时重新计算复杂布局,缓存分组框和网格位置。
- 如果内容只是视觉背景且不需要画布坐标,可考虑
#background。
10. 常见问题
为什么 #canvas 里的按钮点不了?
因为 .rg-map-canvas 默认 pointer-events: none。给按钮加 class="rg-events-all" 或 style="pointer-events:auto",并根据需要阻止事件冒泡。
为什么我的分组框位置和节点对不上?
分组框的 left/top 必须使用画布坐标。不要直接使用 event.clientX/clientY;先用 getCanvasXyByClientXy 或 getCanvasXyByViewXy 换算。
为什么分组框被节点遮住?
#canvas 本来就在节点/连线下方。如果需要显示在节点上方,使用 #canvas-above。如果只是标题被遮住,可以把标题单独放到 #canvas-above 或 #view。
为什么缩放后自定义文字也跟着变大/变小?
这是 #canvas 的预期行为。画布层整体被 scale。若文字希望保持屏幕尺寸不变,使用 #view,或在画布层中按当前 canvasZoom 做反向缩放,但这会增加维护复杂度。