如何在画布上使用 Konva 显示丰富的 HTML
如何显示复杂样式(如粗体)并启用富文本编辑功能?
Canvas 的文本 API 非常有限。Konva.Text 允许您添加多种不同的样式,支持多行文本等。但目前它存在一些限制。您无法为 Konva.Text 的不同部分使用不同的样式。在这种情况下,您必须使用多个 Konva.Text 实例。
如果您想在画布上显示复杂样式,可以使用 render-tag — 一个使用 2D API 直接将 HTML + CSS 渲染到画布上的库。无需 SVG,无需 foreignObject,完全同步。
思路如下:
- 创建一个带有
html属性的自定义Konva.Shape - 使用
render-tag计算布局并将样式文本绘制到画布上下文上 - 形状根据 HTML 内容自动调整高度
说明:尝试在编辑器中输入并格式化文本。格式化后的文本将渲染在其下方的画布上。您可以拖动渲染后的文本。
import Konva from 'konva'; import { Factory } from 'konva/lib/Factory'; // render-tag: 通过纯 2D API 将 HTML+CSS 渲染到画布上 // 从 CDN 加载以避免 Sandpack 转译问题 var computeLayout, drawLayout; // 创建一个自定义 Konva 形状,通过 render-tag 渲染 HTML // 使用 Reflect.construct 从转译后的代码扩展 ES6 类 function RichText(config) { var instance = Reflect.construct(Konva.Shape, [config], RichText); instance._layoutResult = null; instance.on('htmlChange widthChange', function () { this._recomputeLayout(); }); instance._recomputeLayout(); return instance; } RichText.prototype = Object.create(Konva.Shape.prototype); RichText.prototype.constructor = RichText; RichText.prototype.className = 'RichText'; RichText.prototype._recomputeLayout = function () { var html = this.html(); var width = this.width() || 200; if (!html) { this._layoutResult = null; return; } this._layoutResult = computeLayout({ html: html, width: width }); }; RichText.prototype._sceneFunc = function (context) { if (!this._layoutResult) return; var width = this.width() || 200; drawLayout({ layout: this._layoutResult, width: width, ctx: context._context, pixelRatio: 1, }); }; RichText.prototype._hitFunc = function (context) { var width = this.width() || 200; var height = this.height() || (this._layoutResult ? this._layoutResult.height : 0); context.beginPath(); context.rect(0, 0, width, height); context.closePath(); context.fillStrokeShape(this); }; Factory.addGetterSetter(RichText, 'html', ''); Factory.addGetterSetter(RichText, 'width', 200); Factory.addGetterSetter(RichText, 'height', 0); // --- 工具栏 + contenteditable 编辑器 --- function execCmd(cmd, val) { document.execCommand(cmd, false, val || null); editor.focus(); } var toolbar = document.createElement('div'); toolbar.innerHTML = [ '<button onclick="return false" data-cmd="bold"><b>B</b></button>', '<button onclick="return false" data-cmd="italic"><i>I</i></button>', '<button onclick="return false" data-cmd="underline"><u>U</u></button>', '<button onclick="return false" data-cmd="formatBlock" data-val="h1">H1</button>', '<button onclick="return false" data-cmd="formatBlock" data-val="h2">H2</button>', '<button onclick="return false" data-cmd="foreColor" data-val="red" style="color:red">A</button>', ].join(''); toolbar.style.cssText = 'display:flex;gap:4px;margin-bottom:4px;'; toolbar.querySelectorAll('button').forEach(function (btn) { btn.style.cssText = 'padding:2px 8px;cursor:pointer;'; btn.addEventListener('mousedown', function (e) { e.preventDefault(); execCmd(btn.dataset.cmd, btn.dataset.val); }); }); document.body.prepend(toolbar); var editor = document.createElement('div'); editor.contentEditable = true; editor.style.cssText = 'border:1px solid #ccc;padding:8px;min-height:60px;margin-bottom:8px;'; editor.innerHTML = 'That is <u>some</u> <span style="color:red"> styled text</span> on <strong>canvas</strong>!' + '<h2>What do you think about it?</h2>'; var container = document.getElementById('container'); document.body.insertBefore(editor, container); // --- 加载 render-tag 并设置画布 --- var loadScript = function (src) { return new Promise(function (resolve, reject) { var s = document.createElement('script'); s.src = src; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); }; loadScript('https://cdn.jsdelivr.net/npm/render-tag/lib/render-tag.umd.js').then(function () { computeLayout = RenderTag.layout; drawLayout = RenderTag.drawLayout; var stage = new Konva.Stage({ container: 'container', width: window.innerWidth, height: 200, }); var layer = new Konva.Layer(); stage.add(layer); var shape = new RichText({ x: 10, y: 10, width: 400, draggable: true, html: editor.innerHTML, }); layer.add(shape); editor.addEventListener('input', function () { shape.html(editor.innerHTML); }); });