Estamos desenvolvendo uma
plataforma para colaboração visual . Usamos o Canvas para exibir conteúdo: tudo é desenhado nele, incluindo textos. Não existe uma solução pronta para exibir textos no Canvas um a um, como no html. Por vários anos trabalhando com renderização de texto, estudamos várias opções de implementação, preenchemos muitos obstáculos e, ao que parece, encontramos uma boa solução. Vou contar em um artigo como mudamos do Flash para o Canvas e por que abandonamos o SVG ForeignObject.

Movendo-se com o Flash
Criamos o produto em 2015 no Flash. Dentro do Flash, há um editor de texto que pode funcionar bem com textos; portanto, não precisamos fazer nada extra para trabalhar com textos. Mas naquela época o Flash já estava morrendo, então passamos para o HTML / Canvas. Antes de nós, a tarefa era exibir o texto na tela como no editor de html, sem quebrar os textos criados na versão do Flash ao se mover.
Queríamos tornar possível ao usuário editar texto diretamente em nosso produto, sem perceber a transição entre os modos de edição e renderização. A solução que vimos é a seguinte: quando você clica em uma área com texto, um editor de texto é aberto, no qual você pode alterar o texto; Você pode fechar o editor afastando o cursor da área de texto. Nesse caso, a exibição do texto na tela deve 1 em 1 corresponder à exibição do texto no editor.
Como editor, usamos uma biblioteca aberta, mas as bibliotecas prontas para renderizar do html para o Canvas não nos combinam com a velocidade do trabalho e a funcionalidade insuficiente.
Examinamos várias soluções:
- Canvas.fillText padrão. Capaz de desenhar texto como em html, pode ser estilizado, funciona em todos os navegadores. Mas não sabe como desenhar links, como em um editor de html, textos com várias linhas com formatação diferente. Essas dificuldades podem ser resolvidas, mas requerem muito tempo;
- Desenhe o DOM em cima do Canvas. A opção não nos convinha, porque em nosso produto, cada objeto criado possui seu próprio z-index na tela. E misturá-lo com o z-index do DOM não funcionará.
- Converta html em svg. Ele é capaz de transformar html em uma imagem graças ao elemento ForeignObject. Isso permite que você crie o html dentro do svg e trabalhe com ele como uma imagem. Nós escolhemos esta opção.
Funções SVG ForeignObject
Como o SVG ForeignObject funciona: temos HTML do editor → coloque HTML no ForeignObject → alguma mágica → obtenha a imagem → adicione a imagem à tela
Sobre magia. Apesar do fato de a maioria dos navegadores suportar a tag ForeignObject, cada um tem suas próprias características para usar o resultado com canvas. O FireFox funciona com um objeto Blob, no Edge você precisa fazer Base64 para a imagem e retornar dados-url, e no IE11 a tag não funciona.
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 }
Depois de trabalhar com o SVG, tivemos bugs interessantes que não percebemos no Flash. Texto com o mesmo tamanho e fonte em diferentes navegadores foi exibido de maneira diferente. Por exemplo, a última palavra em uma linha pode ser quebrada e executada no texto abaixo. Era importante para nós que os usuários obtivessem o mesmo tipo de widgets, independentemente dos navegadores em que trabalham. Não houve nenhum problema com o Flash, pois ele é o mesmo em todo lugar.

Nós resolvemos esse problema. Primeiro, para todos os textos de linha única, eles começaram a sempre considerar a largura, independentemente do navegador e dos dados do servidor. Para altura, a diferença permanece, mas, no nosso caso, não incomoda os usuários.
Em segundo lugar, experimentalmente chegamos à conclusão de que é necessário adicionar alguns estilos css incomuns para o editor e svg, a fim de reduzir a diferença na exibição entre os navegadores:
- kerning de fonte: auto; controla o kerning da fonte. Mais detalhes
- suavização de fontes da webkit: antialias; responsável pela suavização. Mais detalhes .
O que no final conseguimos graças ao SVG <foreignObject>:
- Podemos desenhar qualquer html: texto, tabelas, gráficos
- A tag retorna uma imagem vetorial.
- A tag funciona em todos os navegadores modernos, exceto no IE11
Por que abandonamos o ForeignObject
Tudo funcionou bem, mas uma vez os designers vieram até nós e pediram para adicionar suporte de fonte para criar modelos.

Nós nos perguntamos se poderíamos fazer isso com o ForeignObject. Acontece que ele tem um recurso que, ao resolver esse problema, se torna uma falha fatal. Ele pode exibir HTML dentro de si, mas não pode acessar recursos externos; portanto, todos os recursos com os quais trabalha devem ser convertidos em base64 e adicionados dentro de svg.

Isso significa que, se você tiver quatro textos escritos pelo OpenSans, precisará fazer o download dessa fonte para o usuário quatro vezes. Esta opção não nos convinha.
Decidimos escrever o nosso Canvas Text com ... bom desempenho, suporte para imagens vetoriais, não esqueceremos o IE 11
Por que uma imagem vetorial é importante para nós? Em nosso produto, qualquer objeto no quadro pode ser ampliado e, com uma imagem vetorial, podemos criá-lo apenas uma vez e reutilizá-lo, independentemente do zoom. O Canvas.fillText desenha um bitmap: nesse caso, precisamos redesenhar a imagem com cada zoom, o que, como pensamos, afeta muito o desempenho.
Crie um protótipo
Primeiro, criamos um protótipo simples para testar seu desempenho.

O princípio de operação do protótipo:
- Damos à função "texto";
- A partir disso, obtemos um objeto no qual existe toda palavra do texto, com coordenadas e estilos para renderização;
- Dê o objeto ao Canvas;
- Tela desenha texto.
O protótipo teve várias tarefas: verificar se o redesenho do Canvas com a escala ocorrerá sem demora e se o tempo para transformar o html em um objeto não será mais do que criar uma imagem svg.
O protótipo lidou com a primeira tarefa, a escala quase não afetou o desempenho ao desenhar textos. Houve problemas com a segunda tarefa: processar grandes quantidades de texto leva tempo suficiente e as primeiras medições de desempenho mostraram resultados ruins. Para desenhar texto de 1 mil caracteres, a nova abordagem levou quase 2 vezes mais tempo que o svg.

Decidimos usar a maneira mais confiável de otimizar o código - “substitua o teste pelo que precisamos” ;-). Mas, falando sério, fomos aos analistas e perguntamos por quanto tempo os textos são criados com mais frequência por nossos usuários. Descobriu-se que o tamanho médio do texto é de 14 caracteres. Para textos tão curtos, nosso protótipo mostrou resultados de desempenho significativamente melhores, como a dependência da velocidade no volume do texto é linear, e o empacotamento no svg é quase sempre feito ao mesmo tempo, independentemente do tamanho do texto. Nos convinha: podemos perder desempenho em textos longos, mas na maioria dos casos nossa velocidade será melhor que svg.
Após várias iterações de trabalho na atualização do Canvas Text, obtivemos o seguinte algoritmo:
Etapa 1. Nós quebramos em blocos lógicos- Dividimos o texto em blocos: parágrafos, listas;
- Dividimos blocos em blocos menores de acordo com os estilos;
- Dividimos os blocos em palavras.
Etapa 2. Coletamos em um objeto com coordenadas e estilos- Conte a largura e a altura de cada palavra em px;
- Nós conectamos as palavras divididas, pois no ponto 2 algumas palavras foram divididas em várias;
- Das palavras que coletamos as linhas, se a palavra não couber na linha, cortamos até ela se encaixar;
- Coletamos parágrafos e listas;
- Calculamos x, y para cada palavra;
- Obtemos um objeto pronto para renderização.
A vantagem dessa abordagem é que podemos cobrir todo o código do HTML para um objeto de texto com testes de unidade. Graças a isso, podemos verificar separadamente a renderização e a análise, o que nos ajudou a acelerar significativamente o desenvolvimento.
Como resultado, apoiamos fontes e o IE 11, cobrimos tudo com testes de unidade e, na maioria dos casos, a velocidade de renderização tornou-se maior que a de ForeignObject. Registrado usuários beta e lançado. Sucesso parece ser!
O sucesso durou 30 minutos
Até agora, pessoas com um sistema de escrita destro não escreveram suporte técnico. Descobrimos que esquecemos a existência de tais idiomas:

Felizmente, não foi difícil adicionar suporte ao sistema de escrita destro, já que o Canvas.fillText padrão já o suporta.
Mas enquanto lidávamos com isso, encontramos casos ainda mais interessantes que o fillText não podia mais suportar. Encontramos textos bidirecionais nos quais parte do texto é escrita da direita para a esquerda, depois da esquerda para a direita e novamente da direita para a esquerda.

A única solução que sabíamos era entrar na especificação do W3C para navegadores e tentar repetir isso dentro do Canvas Text. Foi difícil e doloroso, mas conseguimos adicionar suporte básico. Mais sobre bidirecional:
um e
dois .
Breves conclusões que fizemos por nós mesmos
- Para exibir HTML em uma imagem, use SVG ForeignObject;
- Sempre analise seu produto para tomada de decisão;
- Faça protótipos. Eles podem mostrar que decisões complexas só podem parecer assim à primeira vista;
- Escreva o código imediatamente para que possa ser coberto com testes;
- Em um produto internacional, é importante não esquecer que existem muitos idiomas diferentes, incluindo o bidirecional.
Se você tem experiência na solução de tais problemas - compartilhe nos comentários.