基于 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 场景中,常见的变换操作有以下几种:

  1. 平移(Translation):将图形从一个位置移动到另一个位置。平移可以通过设置图形的 xy 属性来实现。
  2. 缩放(Scaling):改变图形的大小。缩放可以通过设置图形的 scaleXscaleY 属性来实现。
  3. 旋转(Rotation):围绕一个点旋转图形。旋转可以通过设置图形的 rotation 属性来实现。
  4. 倾斜(Skew):使图形的边缘不再垂直或水平。倾斜可以通过设置图形的 skewXskewY 属性来实现。

这些变换操作可以单独应用,也可以组合应用以实现更复杂的变换效果。Konva.js 的变换系统使用矩阵记录移动、缩放、旋转和倾斜等操作。变换可以应用于图形的容器(如 GroupLayer),也可以应用于单个图形。每个图形都有一个变换矩阵,用于描述图形相对于其父容器的变换。

当我们对图形或容器进行变换时,Konva.js 会自动更新图形的变换矩阵,并根据这个矩阵来渲染图形。这样,我们可以轻松地实现复杂的变换操作,而无需手动计算图形的新位置和大小。因此,得益于 Konva.js 的矩阵系统和分层机制,当我们需要某些元素保持不变时,也变得十分方便。

保持指定元素不变的实现方法

反向变换法

反向变换适合在父元素发生变换时使用。其原理是对指定元素施加与父元素变换相反的变换,即使用变换矩阵的逆矩阵,使其位置和形态保持不变。

在我们的项目中,虽然也区分了底图、标注几何信息、标注工具、标注标签等层,但由于这些层之间的位置信息需要联动,所以变换操作是操作的整个场景,标注信息使用了 Konva.Label 来展示,也是场景内的字元素,最终采用了反向变换的方法。Konva.Label 可用于创建带有背景、简单工具提示或带有指针的工具提示的文本。当场景发生变换时,使用逆矩阵可以使标签位置保持不变。

具体实现步骤如下:

  1. 监听 Stagetransform 事件,获取场景变换矩阵。
  2. 计算指定元素的变换矩阵。
  3. 使用 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 元素实现并保持位置不变的例子。用户可以通过鼠标右键点击元素,弹出一个上下文菜单。无论用户如何对画布进行平移、缩放,菜单需要始终固定在鼠标点击的位置。

实现步骤如下:

  1. 监听 Konva.js 的右键点击事件,获取鼠标点击的坐标。
  2. 将鼠标点击的坐标转换为相对于浏览器视口的坐标,并设置右键菜单的位置。
  3. 在图片变换时,重新计算右键菜单的位置,使其始终固定在鼠标点击的位置。

代码示例:

// 获取右键菜单元素
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 元素法保持不变

2d场景

如上图所示,演示中的平移旋转缩放均为初始状态,我们拖动画布改变位置,使用滑块调整缩放和旋转。

变换后的效果如下图所示:
变换后的场景

希望这篇文章对大家有所帮助!

posted @ 2025-01-30 17:04  大河汤汤  阅读(0)  评论(0编辑  收藏  举报