跳到主要内容

使用 Konva 在 HTML5 Canvas 中编辑文本

由于多种原因,用户无法直接编辑 Konva.Text 内容。Canvas API 的设计目的不是编辑文本。 可以在 Canvas 上模拟文本编辑,例如绘制闪烁光标和模拟选择。 Konva 不支持此功能。建议使用 inputtextarea 等原生 DOM 元素,在 Canvas 外部编辑用户输入。

如果要启用完整的富文本编辑功能,请参阅富文本示例

操作说明:双击文本进行编辑。输入内容。按 Enter 或在外部单击以保存更改。

import Konva from 'konva';

const stage = new Konva.Stage({
  container: 'container',
  width: window.innerWidth,
  height: window.innerHeight,
});

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

const textNode = new Konva.Text({
  text: 'Some text here',
  x: 50,
  y: 80,
  fontSize: 20,
  draggable: true,
  width: 200,
});

layer.add(textNode);

const tr = new Konva.Transformer({
  node: textNode,
  enabledAnchors: ['middle-left', 'middle-right'],
  boundBoxFunc: function (oldBox, newBox) {
    newBox.width = Math.max(30, newBox.width);
    return newBox;
  },
});

textNode.on('transform', function () {
  textNode.setAttrs({
    width: textNode.width() * textNode.scaleX(),
    scaleX: 1,
  });
});

layer.add(tr);

textNode.on('dblclick dbltap', () => {
  textNode.hide();
  tr.hide();

  const textPosition = textNode.absolutePosition();
  const stageBox = stage.container().getBoundingClientRect();

  const areaPosition = {
    x: stageBox.left + textPosition.x,
    y: stageBox.top + textPosition.y,
  };

  const textarea = document.createElement('textarea');
  document.body.appendChild(textarea);

  textarea.value = textNode.text();
  textarea.style.position = 'absolute';
  textarea.style.top = areaPosition.y + 'px';
  textarea.style.left = areaPosition.x + 'px';
  textarea.style.width = textNode.width() - textNode.padding() * 2 + 'px';
  textarea.style.height = textNode.height() - textNode.padding() * 2 + 5 + 'px';
  textarea.style.fontSize = textNode.fontSize() + 'px';
  textarea.style.border = 'none';
  textarea.style.padding = '0px';
  textarea.style.margin = '0px';
  textarea.style.overflow = 'hidden';
  textarea.style.background = 'none';
  textarea.style.outline = 'none';
  textarea.style.resize = 'none';
  textarea.style.lineHeight = textNode.lineHeight().toString();
  textarea.style.fontFamily = textNode.fontFamily();
  textarea.style.transformOrigin = 'left top';
  textarea.style.textAlign = textNode.align();
  textarea.style.color = textNode.fill().toString();

  const rotation = textNode.rotation();
  let transform = '';
  if (rotation) {
    transform += 'rotateZ(' + rotation + 'deg)';
  }
  transform += 'translateY(-' + 2 + 'px)';
  textarea.style.transform = transform;

  textarea.style.height = 'auto';
  textarea.style.height = textarea.scrollHeight + 3 + 'px';

  textarea.focus();

  function removeTextarea() {
    textarea.parentNode.removeChild(textarea);
    window.removeEventListener('click', handleOutsideClick);
    window.removeEventListener('touchstart', handleOutsideClick);
    textNode.show();
    tr.show();
    tr.forceUpdate();
  }

  function setTextareaWidth(newWidth = 0) {
    if (!newWidth) {
      newWidth = textNode.placeholder.length * textNode.fontSize();
    }
    textarea.style.width = newWidth + 'px';
  }

  textarea.addEventListener('keydown', function (e) {
    if (e.key === 'Enter' && !e.shiftKey) {
      textNode.text(textarea.value);
      removeTextarea();
    }
    if (e.key === 'Escape') {
      removeTextarea();
    }
  });

  textarea.addEventListener('keydown', function () {
    const scale = textNode.getAbsoluteScale().x;
    setTextareaWidth(textNode.width() * scale);
    textarea.style.height = 'auto';
    textarea.style.height = textarea.scrollHeight + textNode.fontSize() + 'px';
  });

  function handleOutsideClick(e) {
    if (e.target !== textarea) {
      textNode.text(textarea.value);
      removeTextarea();
    }
  }
  setTimeout(() => {
    window.addEventListener('click', handleOutsideClick);
  });
});

textarea 叠加层无法处理的内容

当访问者一次编辑一个字段时,上述方法效果良好。如果文本需要像文档一样工作, 此方法就不再适用:

  • **重排。**调整文本框大小时,叠加层不会重新换行。因此,在变换过程中, 编辑视图与绘制视图不一致。
  • **字体度量。**如果 Web 字体在首次绘制后加载,行高会发生变化。因此, 缓存的度量数据和插入符位置会失效。
  • 单字符样式。Konva.Text 对整个节点应用一种样式。 要在一个段落中混合使用粗体、颜色或字号,需要多个节点和自定义 布局过程。
  • **历史记录。**必须将每次按键操作合并到 Canvas 操作使用的同一个撤销堆栈中。 否则,撤销会在两个不同的时间线之间跳转。
  • **导出保真度。**导出的图像由 Canvas 而不是 textarea 绘制。 因此,两者之间的任何差异都会出现在输出中。

此时,你实际上是在编写文本引擎,而不是编辑器功能。如果不想自行编写,可以使用 Polotno。这是一个使用 Konva 构建的商业设计编辑器 SDK,并且已经解决这些问题。