You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Konva裁剪图层中Transformer与画布(Stage)坐标不匹配问题

问题根源

你遇到的问题本质是:裁剪图层(Layer)设置了x: clipX和y: clipY的全局偏移,但选择逻辑和Transformer的坐标计算直接使用了Stage的全局坐标,没有转换为图层的局部坐标,导致Transformer的定位和图形变换都基于图层原点(而非Stage原点)出现偏移。

解决方案

核心是把所有从Stage获取的鼠标坐标,转换为当前裁剪图层的局部坐标,再用于选择框和元素交互的逻辑。具体修改点如下:

  • 使用layer.globalToLocal()方法,将Stage全局坐标转换成图层局部坐标
  • 选择框是图层内元素,必须用图层局部坐标设置其位置,不能直接用Stage坐标
  • 确保选择框的范围和元素的范围都基于同一坐标系(图层局部)
修改后的完整代码
const Workspace: React.FC = () => {
  const containerRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const width = window.innerWidth;
    const height = window.innerHeight;

    const stage = new Konva.Stage({
      container: containerRef.current!,
      width: width,
      id: 'stage',
      height: height,
    });

    const clipX = (width - 500) / 2;
    const clipY = (height - 500) / 2;
    const canvasHeight = 500;
    const canvasWidth = 500;

    stage.container().style.backgroundColor = '#ededed';

    const layer = new Konva.Layer({
      x: clipX,
      y: clipY,
      clipX: 0,
      clipY: 0,
      clipWidth: canvasWidth,
      clipHeight: canvasHeight,
    });

    stage.add(layer);

    const canvas = new Konva.Rect({
      id: 'canvas',
      fill: 'white',
      height: canvasHeight,
      width: canvasWidth,
      listening: false,
    });

    layer.add(canvas);

    const transformer = new Konva.Transformer({
      id: 'transformer',
    });

    layer.add(transformer);

    const rect1 = new Konva.Rect({
      x: 60,
      y: 60,
      width: 50,
      height: 50,
      fill: 'royalblue',
      name: 'rect',
      id: 'rect1',
      draggable: true,
    });
    layer.add(rect1);

    const text = new Konva.Text({
      id: 'text',
      y: 300,
      x: 300,
      fontSize: 20,
      text: 'Test',
      fill: '#a43',
    });

    layer.add(text);

    // Selection logic
    const selectionRectangle = new Konva.Rect({
      fill: 'rgba(41, 171, 241, 0.5)',
      visible: false,
      listening: false,
    });

    layer.add(selectionRectangle);

    let x1: number, y1: number, x2: number, y2: number, selecting = false;

    stage.on('mousedown touchstart', (e) => {
      if (!['canvas', 'stage'].includes(e.target.attrs.id)) return;

      e.evt.preventDefault();
      // 将Stage坐标转换为图层局部坐标
      const localPos = layer.globalToLocal(stage.getPointerPosition()!);
      x1 = localPos.x;
      y1 = localPos.y;
      x2 = localPos.x;
      y2 = localPos.y;

      selectionRectangle.width(0);
      selectionRectangle.height(0);
      selecting = true;
    });

    stage.on('mousemove touchmove', (e) => {
      if (!selecting) return;

      e.evt.preventDefault();
      // 转换坐标到图层局部
      const localPos = layer.globalToLocal(stage.getPointerPosition()!);
      x2 = localPos.x;
      y2 = localPos.y;

      selectionRectangle.setAttrs({
        visible: true,
        x: Math.min(x1, x2),
        y: Math.min(y1, y2),
        width: Math.abs(x2 - x1),
        height: Math.abs(y2 - y1),
      });
    });

    stage.on('mouseup touchend', (e) => {
      selecting = false;
      if (!selectionRectangle.visible()) return;

      e.evt.preventDefault();
      selectionRectangle.visible(false);

      const shapes = stage.find('Text, .rect');
      // 使用图层局部坐标的选择框范围
      const box = selectionRectangle.getClientRect();
      const selected = shapes.filter((shape) =>
        Konva.Util.haveIntersection(box, shape.getClientRect()),
      );
      transformer.nodes(selected);
    });

    stage.on('click tap', (e) => {
      if (selectionRectangle.visible()) return;

      if (e.target === stage) {
        transformer.nodes([]);
        return;
      }

      if (!e.target.hasName('rect') && e.target.attrs.id !== 'text') return;

      const metaPressed = e.evt.shiftKey || e.evt.ctrlKey || e.evt.metaKey;
      const isSelected = transformer.nodes().indexOf(e.target) >= 0;

      if (!metaPressed && !isSelected) {
        transformer.nodes([e.target]);
      } else if (metaPressed && isSelected) {
        const nodes = transformer.nodes().slice();
        nodes.splice(nodes.indexOf(e.target), 1);
        transformer.nodes(nodes);
      } else if (metaPressed && !isSelected) {
        const nodes = transformer.nodes().concat([e.target]);
        transformer.nodes(nodes);
      }
    });

    return () => {
      stage.destroy();
    };
  }, []);

  return <div ref={containerRef} />;
};
关键修改说明
  • 在mousedown和mousemove事件中,新增layer.globalToLocal()转换鼠标坐标,确保选择框的位置基于图层局部坐标系
  • 调整了元素选择的范围,同时包含Text和rect元素(原代码只选了Text,漏掉了rect)
  • 点击事件中补充了对text元素的选择支持,避免只能选rect的问题

内容的提问来源于stack exchange,提问作者Demi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 23:12:07