在 G6 中,实例化图时指定 container
字段指定了画布 <Canvas></Canvas>
标签的父容器。而 DOM 的坐标与真正绘制图形时的坐标并不是同一套坐标系,这可能会使得如下场景中用户指定坐标时产生困惑:
- 在画布上放置一个
position: absolute
的 DOM 元素,如 tooltip、 menu 等时:- 在鼠标点击画布上的位置放置;
- 鼠标点击节点时,在节点位置。
上面问题在 graph 缩放、平移后更加凸显。下面,我们将深度解析这些坐标之间的关系。
三个坐标系
首先,我们要知道 G6 中有三个坐标系:clientX/clientY、canvasX/canvasY、pointX/pointY。
clientX/clientY
相对于浏览器的坐标系,原点位于浏览器内容范围的左上角(坐标系位置不随滚动条变化)。如下两图中的 y 轴上页面滚动(y-scroll)位置不同,但 clientX/clientY 坐标系的原点都在左上角。因而 Container DOM 的左上角的 clientX/clientY 不同,左图 Container DOM 的左上角坐标为(100, 1000),右图 Container DOM 的左上角坐标为(100, 200)。
canvasX/canvasY
Container DOM 的自身坐标系。假设示例化图时设定 width 与 height 分别时 550 与 500:
const Graph = new G6.Graph({
container: 'container',
width: 550,
height: 500
})
则下图 Container DOM 的宽高即为 550*500。canvasX/canvsY 的原点在 Container DOM 的左上角,Container DOM 右下角的 canvasX/canvasY 坐标为(550,500)。
pointX/pointY
以上两种坐标系均可理解为 DOM 相关的坐标,其取值均为整数。而在真正绘制图形时,图形是根据 pointX/pointY 定位的,即节点的 (x, y) 等都是与pointX/pointY 坐标系相对应的。图的缩放、平移其实是整个 pointX/pointY 坐标系的缩放和平移。
三个坐标系的关系
上面描述比较抽象。下面我们通过具体例子,进一步了解三种坐标系的关系。
下面所有图中,灰色的坐标系表示 clientX/clientY,蓝色坐标系表示 canvasX/canvasY,红色坐标系表示 pointX/pointY。灰、蓝、红色的虚线以及数字均是相应坐标系的标注。
图无变换
下图展示了当图没有缩放和平移,也就是说其变换矩阵 matrix 为单位矩阵时,三个坐标系的关系。可以看到 canvasX/canvasY 与 pointX/pointY 两个坐标系是完全重合的,坐标轴尺度、原点位置完全一致。如下图中树图根节点(黑点标注的)位置,其 canvasX/canvasY 和 pointX/pointY 坐标值是一样的。而 clientX/clientY 的原点在浏览器内容左上角,黑点的 clientX/clientY 则需要加上 Container DOM 的左边距和上边距。
图 1:图无变换时的三种坐标系。
图缩放
当图被缩放:以 pointX/pointY 坐标系的原点为缩放中心,放大为原来的两倍,即图的变换矩阵 matrix 为:
很容易想到,大部分图可能会超出 Container DOM。
事实上,在缩放时,是在缩放 pointX/pointY 的整个坐标系。由于这个例子中缩放中心是 pointX/pointY 坐标系的原点,缩放两倍即是 pointX/pointY 的 x 轴、y 轴的尺度变为原来的两倍。打个比方,我们把 pointX/pointY 的 x 轴、y 轴看作两根绳子,绳子上每隔 1cm 有标记 1、2、3、……这些标记就是 pointX/pointY 的坐标值,我们分别拽住绳子的一端,把它们分别沿正方向拉长一倍,那么相邻标记之间的距离变成了 2cm 。
canvasX/canvasY 和 clientX/clientY 坐标系不随图的变换而变化。换句话说,在这种情况下,canvasX/canvasY 的 (90, 0) 对应的 pointX/pointY 坐标为 (45, 0) ,canvasX/canvasY 的 (0, 250) 对应的 pointX/pointY 坐标为 (0, 125) 。而 clientX/clientY 仍然是 canvasX/canvasY 加上 Container DOM 的左/上边距。
图 2:图缩放变换时的三种坐标系。
可以看到上图中,图 1 中的树图根节点位置(黑点 A)的 canvasX/canvasY 和 clientX/clientY 坐标不变,仍然分别是 (90, 250) 和 (290, 350)。但由于 pointX/pointY 的坐标轴尺度发生了变化,所以它的 pointX/pointY 变为了 (45, 125)。而现在的根节点(黑点 B 标记)的绘制坐标不变,pointX/pointY 仍然是 (90, 250),但黑点 B 对应的 canvasX/canvasY 和 clientX/clientY 分别变为了 (180, 500),(380, 600)。
图缩放+平移
当图在上一节的基础上再进行平移:把图的左上角移动到 (50, 50) 的位置,即图的变换矩阵 matrix 为:
很容易想到,大部分图可能会超出 Container DOM 且左上角为出现留白。
和上一节缩放相似,平移图其实是在平移 pointX/pointY 的整个坐标系。打个比方,我们把 pointX/pointY 的 x 轴、y 轴看作两根绳子,两根绳子一起向右平移 50,向下平移 50。
canvasX/canvasY 和 clientX/clientY 坐标系不随图的变换而变化。换句话说,在这种情况下,canvasX/canvasY 的 (90, 0) 对应的 pointX/pointY 坐标为 ((90-50)/2=20, 0) ,canvasX/canvasY 的 (0, 250) 对应的 pointX/pointY 坐标为 (0, (250-50)/2=100) 。而 clientX/clientY 仍然是 canvasX/canvasY 加上 Container DOM 的左/上边距。
图 3:图缩放+平移变换时的三种坐标系。
可以看到上图中,图 1 中的树图根节点位置(黑点 A)的 canvasX/canvasY 和 clientX/clientY 坐标不变,仍然分别是 (90, 250) 和 (290, 350)。但由于 pointX/pointY 的坐标轴尺度、原点发生了变化,所以它的 pointX/pointY 变为了 (20, 100)。而现在的根节点(黑点 B 标记)的绘制坐标不变,pointX/pointY 仍然是 (90, 250),但黑点 B 对应的 canvasX/canvasY 和 clientX/clientY 分别变为了 (230, 550),(430, 650)。
使用 API 进行转换
了解了三种坐标系的含义后,我们有时需要通过相互转换使用。首先,在 G6 的事件中,event 会包含当前鼠标操作位置的三种坐标值,它们的变量名与上述三种坐标系对应关系如下:
- event.x, event.y => pointX/pointY;
- event.canvasX, event.canvasY => canvasX/canvasY;
- event.clientX, event.clientY => clientX/clientY。
可以发现后两者的名字是直接对应的,我们只需要注意 event 中的 x 和 y 对应的是 pointX/pointY 坐标系即可。
getCanvasByPoint
将 pointX/pointY 坐标系的坐标值转换为 canvasX/canvasY 的坐标值。
getPointByCanvas
将 canvasX/canvasY 坐标系的坐标值转换为 pointX/pointY 的坐标值。
getClientByPoint
将 pointX/pointY 坐标系的坐标值转换为 clientX/clientY 的坐标值。
getPointByClient
将 clientX/clientY 坐标系的坐标值转换为 pointX/pointY 的坐标值。
可以发现 G6 的上述四个 API 都是围绕 point,通过上面四个 API 可以进行组合从而使得 clientX/clientY 与 canvasX/canvasY 进行转换:
clientX/clientY 转 canvasX/canvasY:
const point = graph.getPointByClient(clientX, clientY)
const canvasXY = graph.getCanvasByPoint(point.x, point.y);
canvasX/canvasY 转 clientX/clientY:
const point = graph.getPointByCanvas(canvasX, canvasY)
const clientXY = graph.getClientByPoint(point.x, point.y);
使用三种坐标系
本文开始时,我们提到了如下场景需要我们使用三种坐标系:
在画布上放置一个 position: absolute
的悬浮 DOM 元素,如 tooltip、 menu 等时:
- 在鼠标点击画布上的位置放置;
- 鼠标点击节点时,在节点位置。
如果使用了错误的坐标系来给定悬浮 DOM 元素的位置,将会出现偏移,在图有缩放、平移等变化时,偏移更加严重。
在了解如何使用坐标系给悬浮 DOM 定位前,我们先定义一个悬浮 DOM 元素:
const floatDOM = createDom(`
<div id="test-dom" style="position: absolute; background: #f00; height: 100px; width: 200px">
floating dom
</div>
`);
不论上述哪一种情况,我们都推荐两种挂载这个悬浮 DOM 的方式:
方法一:挂载在 body 上:
document.body.appendChild(floatDOM);
方法二:挂载在 Container DOM 上,即与 canvas 标签同一父容器:
const container = document.getElementById('container') // 假设 Container DOM 的 id 为 container
container.appendChild(floatDOM);
悬浮 DOM 挂载在 body 上
众所周知,position: absolute
的 DOM 元素相对于父容器定位。若我们把悬浮 DOM 挂载在 body 上,它的父容器是 body,我们可以使用 clientX/clientY
来指定它的 left/top
:
在点击画布的位置上放置 DOM:
graph.on('canvas:click', event => {
floatDOM.style.left = event.clientX;
floatDOM.style.top = event.clientY;
});
在某个节点的位置上放置 DOM
const node = graph.getNodes()[0];
const { x, y } = node.getModel(); // 获得该节点的位置,对应 pointX/pointY 坐标
const clientXY = graph.getClientByPoint(x, y);
floatDOM.style.left = clientXY.x;
floatDOM.style.top = clientXY.y;
悬浮 DOM 挂载在 Container DOM 上
若我们把悬浮 DOM 挂载在 Container DOM 上,它的父容器是 Container DOM,我们可以使用 canvasX/canvasY
来指定它的 marginLeft/marginTop
:
在点击画布的位置上放置 DOM:
graph.on('canvas:click', event => {
floatDOM.style.marginLeft = event.canvasX;
floatDOM.style.marginTop = event.canvasY;
});
在某个节点的位置上放置 DOM:
const node = graph.getNodes()[0];
const { x, y } = node.getModel(); // 获得该节点的位置,对应 pointX/pointY 坐标
const canvasXY = graph.getCanvasByPoint(x, y);
floatDOM.style.marginLeft = canvasXY.x;
floatDOM.style.marginTop = canvasXY.y;