Cómo aprendimos a dibujar textos en lienzo

Estamos desarrollando una plataforma para la colaboración visual . Utilizamos Canvas para mostrar contenido: todo se dibuja en él, incluidos los textos. No hay una solución lista para mostrar textos en Canvas uno a uno como en html. Durante varios años de trabajo con la representación de texto, estudiamos varias opciones de implementación, llenamos muchos baches y, al parecer, encontramos una buena solución. Te diré en un artículo cómo nos mudamos de Flash a Canvas y por qué abandonamos SVG foreignObject.



Moviéndose con Flash


Creamos el producto en 2015 en Flash. Dentro de Flash hay un editor de texto que puede funcionar bien con textos, por lo que no tuvimos que hacer nada extra para trabajar con textos. Pero en ese momento Flash ya se estaba muriendo, por lo que pasamos a HTML / Canvas. Y antes de nosotros, la tarea era mostrar el texto en el lienzo como en el editor html, sin romper los textos creados en la versión Flash cuando se mueve.

Queríamos hacerlo para que el usuario pudiera editar el texto directamente en nuestro producto, sin notar la transición entre los modos de edición y renderizado. La solución que vimos es esta: cuando hace clic en un área con texto, se abre un editor de texto en el que puede cambiar el texto; Puede cerrar el editor alejando el cursor del área de texto. En este caso, la visualización del texto en el lienzo debe corresponder 1 en 1 a la visualización del texto en el editor.

Como editor, utilizamos una biblioteca abierta, pero las bibliotecas preparadas para renderizar desde html a Canvas no nos convenían con la velocidad del trabajo y la funcionalidad insuficiente.

Examinamos varias soluciones:

  • Standard Canvas.fillText. Capaz de dibujar texto como en html, se puede diseñar, funciona en todos los navegadores. Pero no sabe cómo dibujar enlaces como en un editor html de textos de varias líneas con un formato diferente. Estas dificultades pueden resolverse, pero requieren mucho tiempo;
  • Dibuja DOM en la parte superior del lienzo. La opción no nos convenía, porque en nuestro producto, cada objeto creado tiene su propio índice z en el lienzo. Y mezclarlo con el índice z DOM no funcionará.
  • Convierte html a svg. Es capaz de convertir html en una imagen gracias al elemento foreignObject. Esto le permite hornear html dentro de svg y trabajar con él como una imagen. Hemos elegido esta opción.

Características SVG foreignObject


Cómo funciona SVG foreignObject: tenemos HTML del editor → poner HTML en foreignObject → algo de magia → obtener la imagen → agregar la imagen al lienzo



Sobre la magia A pesar de que la mayoría de los navegadores admiten la etiqueta foreignObject, cada uno tiene sus propias características para usar el resultado con el lienzo. FireFox funciona con un objeto Blob, en Edge necesita hacer Base64 para la imagen y devolver la URL de datos, y en IE11 la etiqueta no funciona en absoluto.

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 } 

Después de trabajar con SVG, obtuvimos errores interesantes que no notamos en Flash. El texto con el mismo tamaño y fuente en diferentes navegadores se mostró de manera diferente. Por ejemplo, la última palabra en una línea podría ajustarse y ejecutarse en el texto a continuación. Para nosotros era importante que los usuarios obtuvieran el mismo tipo de widgets, independientemente de los navegadores en los que trabajen. No hubo ningún problema con Flash en esto, ya que Él es el mismo en todas partes.



Hemos resuelto este problema. En primer lugar, para todos los textos de una sola línea, comenzaron a considerar siempre el ancho, independientemente del navegador y los datos del servidor. Para la altura, la diferencia sigue siendo, pero en nuestro caso no molesta a los usuarios.

En segundo lugar, experimentalmente llegamos a la conclusión de que es necesario agregar algunos estilos CSS inusuales para el editor y svg para reducir la diferencia de visualización entre los navegadores:

  • interletraje de fuentes: auto; controla el interletraje de la fuente. Más detalles
  • webkit-font-smoothing: antialiased; responsable de alisar. Más detalles

Lo que al final obtuvimos gracias a SVG <foreignObject>:

  • Podemos dibujar cualquier html: texto, tablas, gráficos
  • La etiqueta devuelve una imagen vectorial.
  • La etiqueta funciona en todos los navegadores modernos excepto IE11

¿Por qué abandonamos el objeto extranjero?


Todo funcionó bien, pero una vez que los diseñadores vinieron a nosotros y nos pidieron agregar soporte de fuentes para crear maquetas.



Nos preguntamos si podríamos hacer esto con foreignObject. Resultó que tiene una característica que, al resolver este problema, se convierte en un defecto fatal. Puede mostrar HTML dentro de sí mismo, pero no puede acceder a recursos externos, por lo que todos los recursos con los que trabaja deben convertirse a base64 y agregarse dentro de svg.



Esto significa que si tiene cuatro textos escritos por OpenSans, debe descargar esta fuente al usuario cuatro veces. Esta opción no nos convenía.

Decidimos que escribiríamos nuestro Texto de lienzo con ... buen rendimiento, soporte para imágenes vectoriales, no nos olvidaremos de IE 11

¿Por qué es importante para nosotros una imagen vectorial? En nuestro producto, cualquier objeto en el tablero se puede hacer zoom, y con una imagen vectorial podemos crearlo solo una vez y reutilizarlo independientemente del zoom. Canvas.fillText dibuja un mapa de bits: en este caso, necesitamos volver a dibujar la imagen con cada zoom, lo que, como pensamos, afecta en gran medida el rendimiento.

Crea un prototipo


En primer lugar, creamos un prototipo simple para probar su rendimiento.



El principio de funcionamiento del prototipo:

  • Damos a la función "texto";
  • De él obtenemos un objeto en el que hay cada palabra del texto, con coordenadas y estilos para renderizar;
  • Dale el objeto a Canvas;
  • Lienzo dibuja texto.

El prototipo tenía varias tareas: verificar que el rediseño del lienzo con la escala se llevara a cabo sin demora y que el tiempo para convertir html en un objeto no sería más que crear una imagen svg.

El prototipo hizo frente a la primera tarea, el escalado casi no afectó el rendimiento al dibujar textos. Hubo problemas con la segunda tarea: procesar grandes cantidades de texto lleva suficiente tiempo y las primeras mediciones de rendimiento mostraron malos resultados. Para dibujar texto de 1K caracteres, el nuevo enfoque tomó casi 2 veces más tiempo que svg.


Decidimos utilizar la forma más confiable para optimizar el código: "reemplazar la prueba con la que necesitamos" ;-). Pero en serio, acudimos a los analistas y les preguntamos durante cuánto tiempo nuestros usuarios crean los textos con mayor frecuencia. Resultó que el tamaño promedio del texto es de 14 caracteres. Para textos tan cortos, nuestro prototipo mostró resultados de rendimiento significativamente mejores, ya que la dependencia de la velocidad con el volumen del texto es lineal, y el ajuste en svg casi siempre se realiza al mismo tiempo, independientemente de la longitud del texto. Nos conviene: podemos perder rendimiento en textos largos, pero en la mayoría de los casos nuestra velocidad será mejor que svg.


Después de varias iteraciones de trabajo en la actualización de Canvas Text, obtuvimos el siguiente algoritmo:

Etapa 1. Nos dividimos en bloques lógicos.

  1. Dividimos el texto en bloques: párrafos, listas;
  2. Rompemos bloques en bloques más pequeños según los estilos;
  3. Rompemos los bloques en palabras.

Etapa 2. Recolectamos en un objeto con coordenadas y estilos.

  1. Cuente el ancho y alto de cada palabra en px;
  2. Conectamos las palabras divididas, ya que en el punto 2 algunas palabras se dividieron en varias;
  3. De las palabras recogemos las líneas, si la palabra no cabe en la línea, cortamos hasta que encaje;
  4. Recopilamos párrafos y listas;
  5. Calculamos x, y para cada palabra;
  6. Obtenemos un objeto listo para renderizar.

La ventaja de este enfoque es que podemos cubrir todo el código desde HTML a un objeto de texto con pruebas unitarias. Gracias a esto, podemos verificar por separado el renderizado y el análisis en sí, lo que nos ayudó a acelerar significativamente el desarrollo.

Como resultado, hicimos soporte para fuentes e IE 11, cubrimos todo con pruebas unitarias, y la velocidad de renderizado en la mayoría de los casos se volvió más alta que la de ForeignObject. Registrado en usuarios beta y liberado. El éxito parece ser!

El éxito duró 30 minutos.


Hasta ahora, los hombres con un sistema de escritura diestro no han escrito soporte técnico. Resultó que nos olvidamos de la existencia de tales idiomas:



Afortunadamente, agregar soporte para el sistema de escritura diestro no fue difícil, ya que el Canvas.fillText estándar ya lo admite.

Pero mientras estábamos lidiando con esto, nos encontramos con casos aún más interesantes que fillText ya no podía soportar. Nos encontramos con textos bidireccionales en los que parte del texto está escrito de derecha a izquierda, luego de izquierda a derecha y nuevamente de derecha a izquierda.



La única solución que sabíamos era entrar en la especificación W3C para navegadores e intentar repetir esto dentro de Canvas Text. Fue difícil y doloroso, pero pudimos agregar soporte básico. Más sobre bidireccional: uno y dos .

Breves conclusiones que hicimos por nosotros mismos


  1. Para mostrar HTML en una imagen, use SVG foreignObject;
  2. Siempre analice su producto para la toma de decisiones;
  3. Haz prototipos. Pueden mostrar que las decisiones complejas solo pueden parecerlo a primera vista;
  4. Escriba el código de inmediato para que pueda cubrirse con pruebas;
  5. En un producto internacional, es importante no olvidar que hay muchos idiomas diferentes, incluido el bidereccional.

Si tiene experiencia en la resolución de tales problemas, compártalos en los comentarios.

Source: https://habr.com/ru/post/458624/


All Articles