Skip to main content

使用 Konva 在 HTML5 canvas 中编辑文本

由于多种原因,用户无法直接编辑 Konva.Text 的内容。Canvas API 并不是为文本编辑而设计的。 可以模拟在 canvas 上编辑文本(通过绘制闪烁的光标、模拟选择等)。 Konva 不支持这种情况。我们建议使用 inputtextarea 等原生 DOM 元素,在 canvas 外部编辑用户输入。

如果你想启用完整的富文本编辑功能,请查看 Rich Text Demo

说明:双击文本进行编辑。输入一些内容。按 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({
nodes: [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,已经解决了这些问题。