canvas绘制文本_我们如何学习在HTML5 Canvas上绘制文本

canvas绘制文本

an online collaborative whiteboard, and we are using Canvas to display all graphics and text on boards. There is no out-of-the-box solution for displaying text on Canvas as you would in regular HTML. Hence, over the past several years, we’ve been iterating on this; and we finally think we’ve reached the best solution. 一个在线协作白板 ,并且正在使用Canvas在板上显示所有图形和文本。 没有像在常规HTML中那样在画布上显示文本的现成解决方案。 因此,在过去的几年中,我们一直在对此进行迭代。 最后我们认为我们已经达到了最佳解决方案。

In this article, we will walk you through this transition from Flash to Canvas and why we gave up on using SVG foreignObject in the process.

在本文中,我们将引导您完成从Flash到Canvas的过渡,以及为什么我们放弃在此过程中使用SVG foreignObject的原因。

image

从Flash过渡 (Transition from Flash)

In 2015, we were making a product using Flash. Flash has a built-in text editor that works well, so we didn’t have to do anything extra with that. But at that time, Flash was already dying, and eventually, we decided to move to HTML5 Canvas. The task we were faced with was the following: we needed to draw text on Canvas exactly as in regular HTML while also keeping all text that was previously made in Flash.

2015年,我们使用Flash制作了产品。 Flash具有一个运行良好的内置文本编辑器,因此我们无需为此做任何其他事情。 但是那时,Flash已经快要死了,最终,我们决定转向HTML5 Canvas。 我们面临的任务如下:我们需要像在普通HTML中一样在Canvas上绘制文本,同时还要保留以前在Flash中制作的所有文本。

We wanted to allow the user to edit text directly in our product, without noticing the transition between editing and viewing modes. We were thinking about a text editor that appears when the user clicks on a text area, and that can be closed by moving the cursor away from it. The text displayed in that editor, however, should look identical to the text drawn on Canvas.

我们希望允许用户直接在我们的产品中编辑文本,而无需注意编辑和查看模式之间的转换。 我们正在考虑一个文本编辑器,该文本编辑器在用户单击文本区域时出现,并且可以通过将光标移离文本区域来关闭。 但是,在该编辑器中显示的文本应看起来与“画布”上绘制的文本相同。

We used an open source library for the editor. Still, we were not comfortable with the existing solutions for rendering HTML elements on Canvas due to performance issues and the lack of functionality.

我们为编辑器使用了一个开源库。 但是,由于性能问题和功能不足,我们对在Canvas上呈现HTML元素的现有解决方案并不满意。

We considered several solutions:

我们考虑了几种解决方案:

  • 使用Canvas.fillText()。 (Use Canvas.fillText().)

    It draws text the same as regular HTML, can be stylized and works in all browsers. Yet it doesn’t support multiline text with variable formatting. This problem can be solved, but it requires a significant time investment.

    它可以绘制与常规HTML相同的文本,可以进行样式化并且可以在所有浏览器中使用。 但是,它不支持可变格式的多行文本。 这个问题可以解决,但是需要大量的时间投入。

  • 在画布上绘制DOM。 (Draw DOM over Canvas.)

    This option did not work for us, because in our product, each object on Canvas has a z-index, which conflicts with z-index in DOM.

    此选项对我们不起作用,因为在我们的产品中,Canvas上的每个对象都有一个z-index,该索引与DOM中的z-index冲突。

  • 将HTML转换为SVG。 (Convert HTML to SVG.)

    HTML can be converted to an image using a foreignObject element. It allows us to bake HTML inside SVG and work with it as an image. This is the option we chose.

    可以使用foreignObject元素将HTML转换为图像。 它使我们能够在SVG内部烘焙HTML并将其作为图像使用。 这是我们选择的选项。

使用SVG foreignObject (Working with SVG foreignObject)

SVG foreignObject工作流程如下: (The SVG foreignObject workflow is as follows:)

we have the HTML code from the editor -> we put code into foreignObject -> magic happens -> we get an image -> we put that image on Canvas.

我们有来自编辑器HTML代码->我们将代码放入了ForeignObject->发生了魔术->我们得到了一个图像->我们将该图像放在了Canvas上。

关于魔术部分。 (About that magic part.)

While most browsers support foreignObject, each browser has its own set of quirks when it comes to passing the data to Canvas. Firefox uses Blob object; in Edge, you need to encode the image as a Base64 string and use the data URI scheme; and in IE11, foreignObject doesn’t work at all.

尽管大多数浏览器都支持foreignObject,但在将数据传递到Canvas时,每个浏览器都有其自己的怪癖。 Firefox使用Blob对象; 在Edge中,您需要将图像编码为Base64字符串,并使用数据URI方案; 在IE11中,foreignObject根本不起作用。

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
}

After that work, we found some interesting bugs we hadn’t seen in Flash. Text with the same font and size was displayed differently in different browsers. For example, the last word on a line could wrap and overlap the text on the next line. It was important to us that users get widgets displayed the same, regardless of the browser they use. There was no problem with that in Flash since it is the same everywhere.

完成这项工作后,我们发现了一些Flash中未发现的有趣的错误。 具有相同字体和大小的文本在不同浏览器中的显示方式有所不同。 例如,一行上的最后一个单词可以换行并将文本与下一行重叠。 对我们来说很重要的是,无论用户使用什么浏览器,都必须使用户的小部件显示相同。 Flash中没有问题,因为到处都是相同的。

We did solve this problem. First, we started calculating the width of each text line independently of browser and server data. The difference in height remained, but it doesn’t bother the users in our case.

我们确实解决了这个问题。 首先,我们开始独立于浏览器和服务器数据计算每个文本行的宽度。 高度的差异仍然存在,但在我们的案例中并没有打扰到用户。

Second, experimentally we found that we needed to add some unusual CSS properties to the editor and the SVG element to reduce the display difference between browsers:

其次,通过实验发现,我们需要向编辑器和SVG元素添加一些不寻常CSS属性,以减少浏览器之间的显示差异:

  • font-kerning: auto; which controls the kerning of the font. More information

    字距调整:自动; 它控制字体的字距调整。 更多信息

  • webkit-font-smoothing: antialiased; which controls the antialiasing of the font. More information.

    webkit-font-smoothing:抗锯齿; 它控制字体的抗锯齿。 更多信息

What we got from SVG foreignObject in the end:

最后,我们从SVG foreignObject中获得了什么:

  • We can use any content: text, tables, charts

    我们可以使用任何内容:文本,表格,图表
  • foreignObject returns an image in vector format

    foreignObject返回矢量格式的图像
  • It works in all modern browsers except IE11.

    它适用于除IE11之外的所有现代浏览器。

为什么我们离开foreignObject (Why we moved away from foreignObject)

Everything was going well, but one day our designers came to us and asked us to add font support for their mockups.

一切进展顺利,但是有一天,我们的设计师来找我们,要求我们为他们的模型添加字体支持。

We wondered if this could be done using foreignObject. It turned out that foreignObject has a particular feature that becomes a fatal flaw in this case. It can display HTML, but it cannot access external resources, so they need to be encoded as a Base64 string and added inside the SVG.

我们想知道是否可以使用foreignObject完成。 事实证明,foreignObject具有特定功能,在这种情况下,它成为致命的缺陷。 它可以显示HTML,但不能访问外部资源,因此需要将它们编码为Base64字符串并添加到SVG内。

This means that if you have four text objects that use OpenSans as a font, that font needs to be downloaded four times by the user. This approach did not suit us.

这意味着,如果您有四个使用OpenSans作为字体的文本对象,则该字体需要由用户下载四次。 这种方法不适合我们。

We decided to implement our own Canvas Text, with good performance, vector image, and IE11 support.

我们决定实施自己的Canvas Text,并具有良好的性能,矢量图像和IE11支持。

Why do we need vector images? Each object in our product can be zoomed, and with vector graphics, we don’t have to create additional images for different levels of zoom. In the case of Canvas.fillText(), which draws a raster image, we have to redraw it each time the zoom level is changed, which we thought had a significant impact on performance.

为什么我们需要矢量图像? 我们产品中的每个对象都可以缩放,并且使用矢量图形,我们不必为不同的缩放级别创建其他图像。 对于绘制栅格图像的Canvas.fillText(),每次更改缩放级别时我们都必须重新绘制它,我们认为这会对性能产生重大影响。

创建原型 (Creating a prototype)

The first thing we did was a simple prototype to test the performance.

我们要做的第一件事是测试性能的简单原型。

How this prototype worked:

该原型如何工作:

  • We send a text to a function

    我们向功能发送文本
  • The function returns an object, which contains every word from the received text with coordinates and styles for drawing

    该函数返回一个对象,其中包含接收到的文本中的每个单词,以及用于绘制的坐标和样式
  • We send this object to Canvas

    我们将此对象发送到Canvas
  • Canvas draws the text.

    画布绘制文本。

The prototype had two goals: to check that there is no delay when Canvas redraws the objects on zoom and that the time it takes to turn HTML into an object does not exceed the time it takes to make an image out of SVG.

该原型有两个目标:检查Canvas在缩放状态下重画对象时没有延迟,并且将HTML转换为对象所花费的时间不超过从SVG中制作图像所花费的时间。

The first goal was met successfully. Zooming hardly had any impact on the performance. But there were problems with reaching the second goal: processing of a large enough text took a significant amount of time, and the first performance measures yielded poor results. The new approach took almost twice as long as SVG to draw a 1000-character text.

第一个目标成功实现。 缩放几乎不会对性能产生任何影响。 但是,实现第二个目标存在一些问题:处理足够多的文本需要花费大量时间,而第一个性能指标产生的效果很差。 绘制1000个字符的文本所需的新时间几乎是SVG的两倍。

We decided to use the most reliable method of code optimization: replacing the testing method with one we like.

我们决定使用最可靠的代码优化方法:用我们喜欢的方法代替测试方法。

We went to our analysts and asked them about the most common text length that our users use. It turned out that the average text length was 14 characters. For short texts like this, our prototype showed significantly better results because it has a linear slowdown as the text length is increased while the SVG wrapping took almost always the same amount of time, regardless of the text length used. We were okay with that: we can safely lose large-text performance if, on average, this approach would be faster than using SVG.

我们去了分析师那里,问他们关于用户使用的最常见的文本长度。 事实证明,平均文本长度为14个字符。 对于这样的短文本,我们的原型显示出明显更好的结果,因为它随文本长度的增加而线性下降,而SVG换行几乎总是花费相同的时间,而与所使用的文本长度无关。 我们对此表示同意:如果平均而言,这种方法比使用SVG更快,则可以安全地损失大文本性能。

After several variations of updating Canvas Text, we arrived at the following algorithm:

在更新了Canvas Text的几种变体之后,我们得出以下算法:

步骤1.将文本分成逻辑块 (Step 1. Break the text into logical chunks)

  • Split the text into blocks: paragraphs, lists, etc.

    将文本分成块:段落,列表等。
  • Split the blocks into smaller blocks according to the style used

    根据使用的样式将块分割成较小的块
  • Split the small blocks into words.

    将小块分割成单词。

步骤2.将块连接到具有正确坐标和样式的单个对象中 (Step 2. Join the chunks into a single object with the correct coordinates and styles)

  • Calculate the width and height of every word

    计算每个单词的宽度和高度
  • Join words that were broken apart after substep 2 of the previous step

    在上一步的子步骤2之后合并被分解的单词
  • Make a line out of words; if a word doesn’t fit on the line, cut it until it does

    用言语打成一行; 如果一个单词不适合行,将其剪切直到适合
  • Make paragraphs and lists

    制作段落和列表
  • Calculate X and Y for each word

    计算每个单词的X和Y
  • Get a full object for drawing.

    获取完整的绘图对象。

One advantage of this approach is that we can cover all our code, from HTML to making a text object, with unit tests. Thanks to this, we can check the parsing and the drawing steps separately, which helped us to significantly speed up the development process.

这种方法的一个优势是,我们可以使用单元测试覆盖从HTML到制作文本对象的所有代码。 因此,我们可以分别检查解析和绘制步骤,这有助于我们显着加快开发过程。

In the end, we added font support, IE11 support, and covered everything with unit tests, and the drawing speed has, in most cases, increased compared to foreignObject. We tested this version on our beta users and released it. It seems like a success!

最后,我们添加了字体支持,IE11支持,并用单元测试覆盖了所有内容,并且在大多数情况下,与foreignObject相比,绘制速度有所提高。 我们在Beta版用户中测试了此版本,并发布了该版本。 好像成功了!

成功只持续了30分钟 (Success only lasted 30 minutes)

Until our tech support received messages from the users who use a right-to-left (RTL) writing system. It turned out that we forgot about the existence of such languages.

直到我们的技术支持收到使用从右到左(RTL)书写系统的用户的消息为止。 原来,我们忘记了这种语言的存在。

Fortunately, it wasn’t hard to add support for RTL, since the standard Canvas.fillText() already supports it.

幸运的是,添加对RTL的支持并不困难,因为标准的Canvas.fillText()已经支持它。

But while we were dealing with that, we found even more interesting cases that fillText() didn’t support. We came across bidirectional texts, where part of the text is written from right to left, then from left to right, and then from right to left again.

但是,当我们处理这些问题时,我们发现了fillText()不支持的更有趣的情况。 我们遇到了双向文本,其中部分文本从右向左书写,然后从左向右书写,然后又从右向左书写。

The only solution we knew was to go to the W3C specification for browsers and try to replicate it inside Canvas Text. It was tough and painful, but we managed to add basic support. More info about the bidirectional algorithm here and here.

我们知道的唯一解决方案是转到浏览器的W3C规范,然后尝试在Canvas Text中复制它。 这是艰难而痛苦的,但我们设法增加了基本支持。 有关双向算法的更多信息,请参见此处此处

我们为自己做出的简短结论 (The brief conclusions we have made for ourselves)

  1. Use SVG foreignObject to display HTML as an image.

    使用SVG foreignObject将HTML显示为图像。
  2. Always analyze your product before making a decision.

    在做出决定之前,请务必分析您的产品。
  3. Make prototypes. They can show that what seems to be a complicated solution can actually be pretty straightforward.

    制作原型。 他们可以证明看似复杂的解决方案实际上可以非常简单。
  4. Write the code from the very start in a way that it can be covered with unit tests.

    从一开始就以单元测试可以涵盖的方式编写代码。
  5. In international products, it is important to remember that there are a lot of different languages, including ones with bidirectional writing systems.

    在国际产品中,重要的是要记住,有很多不同的语言,包括带有双向书写系统的语言。

If you have experience in solving such problems, please share it in the comments.

如果您有解决此类问题的经验,请在评论中分享。

P.S.: This article was first published on Medium.

PS:本文最初发表于Medium

翻译自: https://habr.com/en/company/miro/blog/489078/

canvas绘制文本

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值