跳到主要内容

缩放 Canvas 时保持标签与控制柄的屏幕尺寸不变

缩放舞台时,所有内容都会一起缩放——包括你本想用作界面的部分。标签在 20% 时无法辨认, 在 400% 时又大得离谱。拖动控制柄会一直变大,直到盖住它本该调整的图形。

解决办法是按舞台缩放比例的倒数来缩放这些节点,让两者相互抵消。

const s = stage.scaleX();
label.scale({ x: 1 / s, y: 1 / s });

节点在场景坐标中的位置保持不变,因此它仍会跟随所属的图形移动。只有尺寸不再变化。

何时重新计算

请在改变缩放的同一个处理函数中执行。并不存在名为 scale change 的事件——Konva 的属性事件 命名规则是 <attr>Change,因此真正的事件是 scaleXChange

// Correct: fires when scaleX is set.
stage.on('scaleXChange', updateLabels);

// Not a real event. This handler will never run, silently.
stage.on('scale change', updateLabels);

监听 scaleXChange 是可行的,但通常直接在滚轮处理函数中调用你的更新函数会更简单, 而且能把重绘集中在一处。

固定在图形上的标签

滚动以缩放。标签在任何缩放级别都保持可读,而它们所属的图形正常缩放。

import Konva from 'konva';

const stage = new Konva.Stage({
  container: 'container',
  width: 500,
  height: 300,
  draggable: true,
});

const layer = new Konva.Layer();
stage.add(layer);

const shapes = [
  { x: 90, y: 90, radius: 45, fill: '#2f6df6', name: 'Alpha' },
  { x: 250, y: 170, radius: 35, fill: '#10784f', name: 'Beta' },
  { x: 390, y: 100, radius: 40, fill: '#d6336c', name: 'Gamma' },
];

const labels = [];

shapes.forEach((s) => {
  layer.add(new Konva.Circle({ x: s.x, y: s.y, radius: s.radius, fill: s.fill }));

  // The label sits in scene coordinates, so it moves with its circle.
  const label = new Konva.Text({
    x: s.x,
    y: s.y + s.radius + 6,
    text: s.name,
    fontSize: 14,
    fill: '#333',
  });
  label.offsetX(label.width() / 2);
  labels.push(label);
  layer.add(label);
});

// Undo the stage scale on every label.
function keepLabelsReadable() {
  const s = stage.scaleX();
  labels.forEach((label) => label.scale({ x: 1 / s, y: 1 / s }));
}

const hint = new Konva.Text({
  x: 10,
  y: 10,
  text: 'scroll to zoom, drag to pan',
  fontSize: 12,
  fill: '#888',
});
layer.add(hint);

stage.on('wheel', (e) => {
  e.evt.preventDefault();

  const oldScale = stage.scaleX();
  const pointer = stage.getPointerPosition();

  const pointTo = {
    x: (pointer.x - stage.x()) / oldScale,
    y: (pointer.y - stage.y()) / oldScale,
  };

  const direction = e.evt.deltaY > 0 ? -1 : 1;
  const newScale = Math.max(0.2, Math.min(5, oldScale * (direction > 0 ? 1.08 : 1 / 1.08)));

  stage.scale({ x: newScale, y: newScale });
  stage.position({
    x: pointer.x - pointTo.x * newScale,
    y: pointer.y - pointTo.y * newScale,
  });

  keepLabelsReadable();
});

描边宽度是一个特例

描边有自己的开关,因此不需要为它做反向缩放:

shape.strokeScaleEnabled(false);   // the stroke keeps its pixel width when zoomed

Konva.Transformer 的控制柄是普通图形,因此它们确实会随舞台一起缩放。要让它们在缩放过程中 保持可用的尺寸,请根据缩放比例来设置 anchorSize,而不是对整个变换器做反向缩放:

tr.anchorSize(10 / stage.scaleX());

完全忽略缩放的图层

反向缩放让节点的尺寸保持固定,而它的位置仍然跟随场景。对于 HUD——工具栏、比例尺、 坐标读数——你希望两者都固定在屏幕坐标空间中。

每个图层都是舞台的子节点,因此会继承舞台的变换。要抵消的是整个变换,而不只是缩放:

function pinLayerToScreen(layer) {
const s = stage.scaleX();
layer.scale({ x: 1 / s, y: 1 / s });
layer.position({ x: -stage.x() / s, y: -stage.y() / s });
}

这会把图层坐标映射回屏幕坐标:无论舞台如何缩放或平移,该图层上位于 (10, 10) 的节点 始终距离左上角 10 像素。请在调用反向缩放的同一处调用它。

如果 HUD 图层纯粹是装饰性的,请为它设置 listening: false,以免它吞掉本应传给 下方场景的点击。

相关内容