JavaScript is required

画布拓展内容(#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;先用 getCanvasXyByClientXygetCanvasXyByViewXy 换算。

为什么分组框被节点遮住?

#canvas 本来就在节点/连线下方。如果需要显示在节点上方,使用 #canvas-above。如果只是标题被遮住,可以把标题单独放到 #canvas-above#view

为什么缩放后自定义文字也跟着变大/变小?

这是 #canvas 的预期行为。画布层整体被 scale。若文字希望保持屏幕尺寸不变,使用 #view,或在画布层中按当前 canvasZoom 做反向缩放,但这会增加维护复杂度。

11. 下一步阅读