Skip to main content

如何在画布上使用 Konva 显示丰富的 HTML

如何显示复杂样式(如粗体)并启用富文本编辑功能?

Canvas 的文本 API 非常有限。Konva.Text 允许您添加多种不同的样式,支持多行文本等。但目前它存在一些限制。您无法为 Konva.Text 的不同部分使用不同的样式。在这种情况下,您必须使用多个 Konva.Text 实例。

如果您想在画布上显示复杂样式,可以使用 render-tag — 一个使用 2D API 直接将 HTML + CSS 渲染到画布上的库。无需 SVG,无需 foreignObject,完全同步。

思路如下:

  1. 创建一个带有 html 属性的自定义 Konva.Shape
  2. 使用 render-tag 计算布局并将样式文本绘制到画布上下文上
  3. 形状根据 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);
  });
});