# 🎨 当 Canvas 遇见 DOM:在 Konva 中实现丝滑文本编辑的技巧
# 💡 为什么需要 DOM + Canvas 的混合方案?
konva 的 Text
组件虽然强大,但在处理复杂文本编辑时存在局限:
- ⌨️ 缺少自动换行、光标控制等原生能力
- 🖥️ 复杂样式支持有限
解决方案:通过 DOM 元素模拟 Canvas 的文本编辑!
# 📌 目标
让 Konva 画布中的节点文本 双击可编辑,效果像直接在 Canvas 上输入文字一样丝滑!(无需 Konva 的复杂组件,直接用原生 <textarea>
实现!)
# 🔧 核心思路
- 双击触发:监听 konva 节点的
onDblClick
事件 - DOM 定位:Konva 画布坐标系计算 DOM 元素的绝对位置
- 同步样式:让
<textarea>
完全复刻 Konva 节点的样式 (颜色、字体、字号等) - 数据同步:输入完成时更新 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 输入框
- 创建
textarea
- 找到 konva 的位置,并同步到 DOM 中
- 复刻样式
- 聚焦
// 创建 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️⃣ 数据同步与销毁
- 监听事件:监听键盘事件、失焦事件
- 移除 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️⃣ 文本位置获取详解 (向量运算)
stage
的x & y
是整个画布相对于视口的偏移量node
的postion
是当前节点相对于画布的偏移量- 而我们要获取的是当前节点相对于视口的偏移量
- 而
node
实际的相对于画布的偏移量是受scale
影响的 - 所以当前节点相对于视口的偏移量就是:
stage + node * scale
// 获取文本位置 | |
const areaPosition = { | |
x: stage.x() + node.position[0] * scale, | |
y: stage.y() + node.position[1] * scale, | |
}; |
# 总结
通过 DOM + Canvas 的混合方案,实现了一个超自然文本编辑的组件。通过监听 onDblClick
事件,在画布上双击时创建一个 textarea
,并同步样式和位置。通过键盘事件和失焦事件,实现了文本的保存和尺寸的更新。
# 🌈 亮点
- 纯原生 DOM 操作,轻量高效
- 完美融合:输入框与画布元素无缝衔接
🎉 现在,画布节点拥有 “编辑超能力” 了!