# 🎨 当 Canvas 遇见 DOM:在 Konva 中实现丝滑文本编辑的技巧

# 💡 为什么需要 DOM + Canvas 的混合方案?

konva 的 Text 组件虽然强大,但在处理复杂文本编辑时存在局限:

  • ⌨️ 缺少自动换行、光标控制等原生能力
  • 🖥️ 复杂样式支持有限

解决方案:通过 DOM 元素模拟 Canvas 的文本编辑!


# 📌 目标

让 Konva 画布中的节点文本 双击可编辑,效果像直接在 Canvas 上输入文字一样丝滑!(无需 Konva 的复杂组件,直接用原生 <textarea> 实现!)

# 🔧 核心思路

  1. 双击触发:监听 konva 节点的 onDblClick 事件
  2. DOM 定位:Konva 画布坐标系计算 DOM 元素的绝对位置
  3. 同步样式:让 <textarea> 完全复刻 Konva 节点的样式 (颜色、字体、字号等)
  4. 数据同步:输入完成时更新 Konva 节点文本和尺寸

# 📝 代码

# 1️⃣ 监听双击事件

const handleDoubleClick = (e: KonvaEventObject<MouseEvent>) => {
  const stage = e.target.getStage()!;
  // 计算缩放后的位置
  const areaPosition = {
    x: stage.x() + node.position[0] * stage.scaleX(),
    y: stage.y() + node.position[1] * stage.scaleY()
  };
  // 创建 DOM 输入框
  createTextarea(areaPosition);
};

# 2️⃣ 创建 DOM 输入框

  1. 创建 textarea
  2. 找到 konva 的位置,并同步到 DOM 中
  3. 复刻样式
  4. 聚焦
// 创建 textarea
const textarea = document.createElement("textarea");
const container = document.getElementById("konva-container")
if (!container) return;
container.appendChild(textarea);
textarea.value = node.text;
textarea.style.position = "absolute";
textarea.style.top = `${areaPosition.y}px`;
textarea.style.left = `${areaPosition.x}px`;
textarea.style.width = `${node.size[0] * scale}px`;
textarea.style.height = `${node.size[1] * scale}px`;
textarea.style.fontSize = `${16 * scale}px`;
textarea.style.border = "2px solid #4f46e5"; // 复刻 Rect 的 stroke
textarea.style.borderRadius = "8px"; // 复刻 Rect 的 cornerRadius
textarea.style.padding = "20px"; // 复刻 Text 的 padding
textarea.style.margin = "0px";
textarea.style.overflow = "hidden";
textarea.style.background = "#ffffff"; // 复刻 Rect 的 fill
textarea.style.outline = "none";
textarea.style.resize = "none";
textarea.style.wordBreak = "break-word";
textarea.style.fontFamily = "Arial, sans-serif"; // 复刻 Text 的 fontFamily
textarea.style.transformOrigin = "left top";
textarea.style.color = "#000000"; // 复刻 Text 的颜色
textarea.focus();

# 3️⃣ 数据同步与销毁

  1. 监听事件:监听键盘事件、失焦事件
  2. 移除 DOM 元素
function removeTextarea() {
    textarea.remove();
}
// 监听键盘事件
textarea.addEventListener("keydown", (e) => {
    if (e.key === "Enter" && !e.shiftKey) {
        e.preventDefault();
        textarea.blur(); // 失去焦点触发保存
    }
});
textarea.addEventListener("input", () => {
    const [width, height]: [number, number] = measureText(textarea.value);
    textarea.style.width = `${width}px`;
    textarea.style.height = `${height}px`;
})
// 失焦时保存文本
textarea.addEventListener("blur", () => {
    const newText = textarea.value;
    console.log(newText);
    actions.updateNodeText(node.id, newText); // 更新节点文本
    actions.updateNodeSize(node.id, measureText(newText)); // 更新节点尺寸
    removeTextarea(); // 移除 textarea
});

# 4️⃣ 测量文本尺寸

通过直接在 DOM 中创建一个临时的 Konva.Text 元素,然后获取它的尺寸。由于这边复刻的 padding 是 20px 上下左右都有,所以宽高都要加上 40px。
如果知识单单的复刻,就不需要加 padding 了,直接返回宽高就行。

// 测量文本尺寸
const measureText = (text: string): [number, number] => {
    const tempText = new Konva.Text({
        fontSize: 16,
        text: text
    })
    return [
        tempText.width() + 40,
        tempText.height() + 40,
    ];
};

# 5️⃣ 文本位置获取详解 (向量运算)

  1. stagex & y 是整个画布相对于视口的偏移量
  2. nodepostion 是当前节点相对于画布的偏移量
  3. 而我们要获取的是当前节点相对于视口的偏移量
  4. node 实际的相对于画布的偏移量是受 scale 影响的
  5. 所以当前节点相对于视口的偏移量就是: stage + node * scale
// 获取文本位置
const areaPosition = {
    x: stage.x() + node.position[0] * scale,
    y: stage.y() + node.position[1] * scale,
};

# 总结

通过 DOM + Canvas 的混合方案,实现了一个超自然文本编辑的组件。通过监听 onDblClick 事件,在画布上双击时创建一个 textarea ,并同步样式和位置。通过键盘事件和失焦事件,实现了文本的保存和尺寸的更新。

# 🌈 亮点

  • 纯原生 DOM 操作,轻量高效
  • 完美融合:输入框与画布元素无缝衔接

🎉 现在,画布节点拥有 “编辑超能力” 了!