我们正在开发
视觉协作平台 。 我们使用Canvas来显示内容:所有内容都绘制在其中,包括文本。 没有现成的解决方案可以像html一样在Canvas上一对一地显示文本。 在使用文本渲染的几年中,我们研究了各种实现选项,遇到了许多麻烦,并且似乎找到了一个好的解决方案。 我将在一篇文章中告诉您我们是如何从Flash迁移到Canvas的,为什么我们放弃了SVG foreignObject。

使用Flash移动
我们于2015年在Flash上创建了该产品。 在Flash内部,有一个文本编辑器可以很好地处理文本,因此我们不需要做任何额外的工作即可处理文本。 但是那时Flash已经快死了,所以我们从Flash转移到HTML / Canvas。 在我们之前,我们的任务是像在html编辑器中那样在Canvas上显示文本,同时在移动时不破坏在Flash版本中创建的文本。
我们希望使用户可以直接在我们的产品中编辑文本,而无需注意编辑和渲染模式之间的转换。 我们看到的解决方案是:单击带有文本的区域时,将打开一个文本编辑器,您可以在其中更改文本; 您可以通过将光标从文本区域移开来关闭编辑器。 在这种情况下,“画布”上的文本显示应与编辑器中的文本显示一一对应。
作为编辑器,我们使用了一个开放库,但是从html到Canvas渲染的现成库不适合我们的工作速度和功能不足。
我们研究了几种解决方案:
- 标准Canvas.fillText。 能够像html一样绘制文本,可以设置样式,并在所有浏览器中均可使用。 但是,它不知道如何在html编辑器中绘制链接(格式不同)。 这些困难可以解决,但需要很多时间。
- 在画布上绘制DOM。 该选项不适合我们,因为 在我们的产品中,每个创建的对象在画布上都有其自己的z索引。 并将其与DOM z-index混合将不起作用。
- 将html转换为svg。 多亏了foreignObject元素,他才能够将html变成图像。 这使您可以在svg中烘烤html并将其作为图像使用。 我们选择了此选项。
功能SVG foreignObject
SVG foreignObject的工作方式:我们从编辑器中获取HTML→将HTML放入foreignObject中→有点魔术→获得图像→将图像添加到画布
关于魔术。 尽管大多数浏览器都支持foreignObject标记,但每种浏览器都有将其结果与画布一起使用的特征。 FireFox与Blob对象一起使用,在Edge中,您需要对图像进行Base64处理并返回data-url,而在IE11中,标记根本不起作用。
getImageUrl(svg: string, browser: string): string { let dataUrl = '' switch (browser) { case browsers.FIREFOX: let domUrl = window.URL || window.webkitURL || window let blob = new Blob([svg], {type: 'image/svg+xml;charset=utf-8'}) dataUrl = domUrl.createObjectURL(blob) break case browsers.EDGE: let encodedSvg = encodeURIComponent(svg) dataUrl = 'data:image/svg+xml;base64,' + btoa(window.unescape(encodedSvg)) break default: dataUrl = 'data:image/svg+xml,' + encodeURIComponent(svg) return dataUrl }
使用SVG之后,我们得到了一些有趣的错误,这些错误在Flash上没有发现。 在不同浏览器中具有相同大小和字体的文本显示方式有所不同。 例如,一行中的最后一个单词可以换行并插入下面的文本中。 对于我们而言,重要的是,无论用户使用哪种浏览器,都必须获得相同类型的小部件。 Flash没问题,因为 他到处都是一样的。

我们已经解决了这个问题。 首先,对于所有单行文本,无论浏览器和服务器中的数据如何,它们都开始始终考虑宽度。 对于高度,差异仍然存在,但在我们的情况下,不会打扰用户。
其次,通过实验得出的结论是,有必要为编辑器和svg添加一些不常见的CSS样式,以减少浏览器之间的显示差异:
- 字距调整:自动; 控制字体的字距调整。 更多细节
- webkit-font-smoothing:抗锯齿; 负责平滑。 更多细节 。
最终,我们感谢SVG <foreignObject>:
- 我们可以绘制任何html:文本,表格,图形
- 标签返回矢量图像。
- 该标记可在IE11以外的所有现代浏览器中使用
为什么我们放弃foreignObject
一切工作都很好,但是一旦设计师来找我们,并要求增加字体支持来创建模型。

我们想知道是否可以使用foreignObject做到这一点。 事实证明,他具有解决该问题时的致命缺陷。 它可以在其内部显示HTML,但不能访问外部资源,因此必须使用所有资源将其转换为base64并添加到svg中。

这意味着如果您有四个由OpenSans编写的文本,则需要将该字体下载四次给用户。 此选项不适合我们。
我们决定编写具有良好性能的Canvas文本,支持矢量图像,我们不会忘记IE 11
为什么矢量图像对我们很重要? 在我们的产品中,板上的任何对象都可以缩放,并且使用矢量图像,我们只能创建一次,并且无论缩放如何都可以重复使用它。 Canvas.fillText绘制一个位图:在这种情况下,我们需要在每次缩放时重新绘制图像,正如我们认为的那样,这会极大地影响性能。
创建一个原型
首先,我们创建了一个简单的原型来测试其性能。

原型的工作原理:
- 我们给函数“文本”;
- 从中我们得到一个对象,其中的每个单词都来自文本,带有用于渲染的坐标和样式。
- 将对象交给Canvas;
- 画布绘制文本。
该原型有以下几项任务:验证可以按比例缩放Canvas,而不会拖延地进行绘制,并且将html转换为对象的时间不超过创建svg图像。
原型解决了第一个任务,缩放比例几乎不影响绘制文本时的性能。 第二个任务存在问题:处理大量文本需要足够的时间,而第一次的性能测量结果却很差。 从1K个字符绘制文本,新方法花费的时间几乎是svg的2倍。
我们决定使用最可靠的方法来优化代码-“用我们需要的测试替换测试” ;-)。 但是,很认真的说,我们去了分析师那里,问他们最经常由用户创建文本多长时间。 事实证明,平均文本大小为14个字符。 对于这样的简短文本,我们的原型显示出明显更好的性能结果,因为 速度对文本量的依赖性是线性的,并且无论文本的长度如何,在svg中换行几乎总是同时进行。 它适合我们:在长文本上我们可能会失去性能,但是在大多数情况下,我们的速度将比svg好。
在对Canvas Text更新进行几次迭代之后,我们得到了以下算法:
阶段1。我们分为逻辑块- 我们将文本分为几段:段落,列表;
- 我们根据样式将块分成较小的块;
- 我们将块分解成文字。
第2阶段。我们将一个具有坐标和样式的对象收集在一起- 计算每个单词的宽度和高度(以px为单位);
- 我们将分割后的词连接起来,因为在第2点中某些词被分成了几个词;
- 从单词中我们收集线条,如果单词不适合线条,我们将其剪裁至适合;
- 我们收集段落和列表;
- 我们为每个单词计算x,y;
- 我们得到一个现成的对象进行渲染。
这种方法的优点是我们可以使用单元测试覆盖从HTML到文本对象的所有代码。 因此,我们可以分别检查渲染和解析本身,这有助于我们显着加快开发速度。
结果,我们提供了对字体和IE 11的支持,并用单元测试覆盖了所有内容,并且在大多数情况下,渲染速度都高于foreignObject。 检入Beta用户并发布。 成功似乎是!
成功持续了30分钟
到目前为止,使用右手书写系统的人还没有书写技术支持。 原来,我们忘记了这些语言的存在:

幸运的是,添加对惯用右手书写系统的支持并不困难,因为标准的Canvas.fillText已经支持它。
但是当我们处理这个问题时,我们遇到了fillText不再支持的更有趣的情况。 我们遇到了双向文本,其中部分文本从右到左,然后从左到右,再从右到左写。

我们知道的唯一解决方案是进入浏览器的W3C规范,并尝试在Canvas Text中重复此操作。 这是艰难而痛苦的,但是我们能够添加基本的支持。 有关双向的更多信息:
一和
二 。
我们为自己做出的简短结论
- 要在图片中显示HTML,请使用SVG国外对象;
- 始终分析您的产品以进行决策;
- 制作原型。 他们可以证明,乍一看,复杂的决定看起来似乎只有这样。
- 立即编写代码,以便可以进行测试;
- 在国际产品中,重要的是不要忘记存在许多种不同的语言,包括双向语言。
如果您有解决此类问题的经验,请在评论中分享。