基于 Konva.js 的 2D 场景变换中保持指定元素位置不变的方法
基于 Konva.js 的 2D 场景变换中保持指定元素位置不变的方法
大家好,我是前端丁修。这篇文章将介绍在 2D 场景变换操作中,如何保持指定元素位置不变的方法。这是一个机器人公司前端的面试题,也是我实际项目中遇到的一个需求。当时的项目是一个 2D 标注项目,核心功能是用户上传图片,加载到 Canvas 画布中,通过 OCR 识别,对图片中的信息进行提取和打标。用户也可以对图片进行各种变换操作,手动截取需要的部分进行标注。
一个完整的标注由所标注区域的几何信息和文本内容组成。几何信息可以直接渲染到画布上,而文本信息仅在交互需要时显示。无论用户如何操作画布,都需要确保标注的文本信息处于正方向,且大小限制在一定范围内变化。
Konva.js 简介
在项目中,我们选择了 Konva.js 作为 Canvas 渲染引擎。这是一个功能强大且灵活的库,专门为 2D 图形和交互而设计。在 Konva.js 中,所有图形都位于一个 Stage
上,Stage
可以包含多个 Layer
,每个 Layer
又可以包含多个 Shape
。具体的使用方法可以参考官网文档。这里主要介绍坐标系和变换操作。
坐标系
Konva.js 的坐标系与 Canvas 一致,x 轴向右,y 轴向下。坐标系的原点 (0, 0)
位于画布的左上角。所有图形的位置都是相对于这个坐标系的。
变换系统
在 2D 场景中,常见的变换操作有以下几种:
- 平移(Translation):将图形从一个位置移动到另一个位置。平移可以通过设置图形的
x
和y
属性来实现。 - 缩放(Scaling):改变图形的大小。缩放可以通过设置图形的
scaleX
和scaleY
属性来实现。 - 旋转(Rotation):围绕一个点旋转图形。旋转可以通过设置图形的
rotation
属性来实现。 - 倾斜(Skew):使图形的边缘不再垂直或水平。倾斜可以通过设置图形的
skewX
和skewY
属性来实现。
这些变换操作可以单独应用,也可以组合应用以实现更复杂的变换效果。Konva.js 的变换系统使用矩阵记录移动、缩放、旋转和倾斜等操作。变换可以应用于图形的容器(如 Group
或 Layer
),也可以应用于单个图形。每个图形都有一个变换矩阵,用于描述图形相对于其父容器的变换。
当我们对图形或容器进行变换时,Konva.js 会自动更新图形的变换矩阵,并根据这个矩阵来渲染图形。这样,我们可以轻松地实现复杂的变换操作,而无需手动计算图形的新位置和大小。因此,得益于 Konva.js 的矩阵系统和分层机制,当我们需要某些元素保持不变时,也变得十分方便。
保持指定元素不变的实现方法
反向变换法
反向变换适合在父元素发生变换时使用。其原理是对指定元素施加与父元素变换相反的变换,即使用变换矩阵的逆矩阵,使其位置和形态保持不变。
在我们的项目中,虽然也区分了底图、标注几何信息、标注工具、标注标签等层,但由于这些层之间的位置信息需要联动,所以变换操作是操作的整个场景,标注信息使用了 Konva.Label
来展示,也是场景内的字元素,最终采用了反向变换的方法。Konva.Label
可用于创建带有背景、简单工具提示或带有指针的工具提示的文本。当场景发生变换时,使用逆矩阵可以使标签位置保持不变。
具体实现步骤如下:
- 监听
Stage
的transform
事件,获取场景变换矩阵。 - 计算指定元素的变换矩阵。
- 使用
Konva.Node.prototype.setAbsoluteTransform()
方法对指定元素施加反向变换矩阵。
stage.on('transform', function () {
// 获取场景变换矩阵
var stageTransform = stage.getAbsoluteTransform().copy();
// 计算指定元素的反向变换矩阵
var inverseTransform = stageTransform.invert();
// 对指定元素施加反向变换矩阵
targetNode.setAbsoluteTransform(inverseTransform);
});
这种方法的优点是可以精确抵消父元素的任何变换,包括平移、缩放、旋转等复合变换。
分层处理法
这种方法适用于场景不可变换时,目标元素和其他元素弱关联的情况。可以对场景内的元素分层,只对目标层进行变换,保持其他层不变。
在某些项目中,需要在画布上显示性能信息(如 FPS)、数据统计(如元素数量)或调试信息(如鼠标坐标、缩放比例等)。无论用户如何操作画布,这些信息需要始终固定在屏幕的某个位置(例如右上角),不会随画布的变换而移动。
我们可以将场景元素放在一个 Layer
中,而将数据统计或性能信息的显示区域放在另一个 Layer
中。然后,只对图片所在的 Layer
进行变换操作,统计信息显示区域所在的 Layer
保持不变。
代码示例:
// 创建两个 Layer
var imageLayer = new Konva.Layer(); // 图片和标注层
var statsLayer = new Konva.Layer(); // FPS/元素数显示层
// 将图片和标注内容添加到图片层
imageLayer.add(image);
imageLayer.add(annotation1);
imageLayer.add(annotation2);
// 创建 FPS/元素数显示区域
var statsText = new Konva.Text({
x: stage.width() - 150, // 固定在右上角
y: 10,
text: 'FPS: 60 | Elements: 0',
fontSize: 14,
fontFamily: 'Arial',
fill: 'white',
padding: 5,
align: 'right'
});
// 将 FPS/元素数显示区域添加到 statsLayer
statsLayer.add(statsText);
// 将 Layer 添加到 Stage
stage.add(imageLayer);
stage.add(statsLayer);
// 监听图片层的变换事件
imageLayer.on('transform', function () {
// 对图片层进行变换操作
imageLayer.scale(stage.scale());
imageLayer.x(stage.x());
imageLayer.y(stage.y());
});
// statsLayer 保持不变
statsLayer.setAbsolutePosition({ x: 0, y: 0 });
// 更新 FPS/元素数显示
function updateStats() {
var fps = calculateFPS(); // 计算当前帧率
var elementCount = imageLayer.children.length; // 获取当前元素数量
statsText.text(`FPS: ${fps} | Elements: ${elementCount}`);
statsLayer.batchDraw(); // 仅重绘 statsLayer
requestAnimationFrame(updateStats);
}
// 启动 FPS/元素数更新
updateStats();
DOM 方法
还有一种思路,可以将标签使用 DOM 元素与定位的方式显示在画布之上。这种方法适合需要对标签文本进行直接编辑的情况,可以借助 DOM 的交互能力,无需在 Canvas 中模拟实现输入框。
选中图形后的操作菜单也是一个非常适合用 DOM 元素实现并保持位置不变的例子。用户可以通过鼠标右键点击元素,弹出一个上下文菜单。无论用户如何对画布进行平移、缩放,菜单需要始终固定在鼠标点击的位置。
实现步骤如下:
- 监听 Konva.js 的右键点击事件,获取鼠标点击的坐标。
- 将鼠标点击的坐标转换为相对于浏览器视口的坐标,并设置右键菜单的位置。
- 在图片变换时,重新计算右键菜单的位置,使其始终固定在鼠标点击的位置。
代码示例:
// 获取右键菜单元素
var contextMenu = document.getElementById('context-menu');
// 监听 Stage 的右键点击事件
stage.on('contextmenu', function (e) {
// 阻止默认的右键菜单
e.evt.preventDefault();
// 获取鼠标点击的坐标(相对于 Stage)
var pointerPos = stage.getPointerPosition();
// 将 Stage 坐标转换为浏览器视口坐标
var stageContainer = stage.container(); // 获取 Stage 的容器
var containerRect = stageContainer.getBoundingClientRect();
var x = pointerPos.x + containerRect.left;
var y = pointerPos.y + containerRect.top;
// 设置右键菜单的位置并显示
contextMenu.style.left = x + 'px';
contextMenu.style.top = y + 'px';
contextMenu.style.display = 'block';
});
// 监听文档的点击事件,隐藏右键菜单
document.addEventListener('click', function () {
contextMenu.style.display = 'none';
});
通过以上方法,我们可以在 2D 场景变换中灵活地保持指定元素的位置不变。无论是使用反向变换、分层处理还是 DOM 方法,都可以根据具体需求选择最适合的方案。最后,我制作了一个简单的演示,在这个演示中:
- 浅蓝色方块代表普通元素,会随场景变换而变换
- 浅绿色矩形使用反向变换法保持不变
- 浅粉色矩形使用分层处理法保持不变
- 右上角的信息框使用 DOM 元素法保持不变
如上图所示,演示中的平移旋转缩放均为初始状态,我们拖动画布改变位置,使用滑块调整缩放和旋转。
变换后的效果如下图所示:
希望这篇文章对大家有所帮助!