Nous développons une
plateforme de collaboration visuelle . Nous utilisons Canvas pour afficher le contenu: tout y est dessiné, y compris les textes. Il n'y a pas de solution toute faite pour afficher les textes sur Canvas un à un comme en html. Pendant plusieurs années de travail avec le rendu de texte, nous avons étudié diverses options d'implémentation, rempli beaucoup de bosses et, semble-t-il, trouvé une bonne solution. Je vais vous expliquer dans un article comment nous sommes passés de Flash à Canvas et pourquoi nous avons abandonné SVG foreignObject.

Se déplacer avec Flash
Nous avons créé le produit en 2015 sur Flash. Dans Flash, il y a un éditeur de texte qui peut bien fonctionner avec des textes, nous n'avons donc pas eu besoin de faire quoi que ce soit de plus pour travailler avec des textes. Mais à ce moment-là, Flash était déjà en train de mourir, alors nous en sommes passés à HTML / Canvas. Et avant nous, la tâche consistait à afficher le texte sur le canevas comme dans l'éditeur html, sans casser les textes créés dans la version Flash lors du déplacement.
Nous voulions faire en sorte que l'utilisateur puisse éditer le texte directement dans notre produit, sans remarquer la transition entre les modes d'édition et de rendu. La solution que nous avons trouvée est la suivante: lorsque vous cliquez sur une zone de texte, un éditeur de texte s'ouvre dans lequel vous pouvez modifier le texte; Vous pouvez fermer l'éditeur en éloignant le curseur de la zone de texte. Dans ce cas, l'affichage du texte sur le canevas doit 1 en 1 correspondre à l'affichage du texte dans l'éditeur.
En tant qu'éditeur, nous avons utilisé une bibliothèque ouverte, mais les bibliothèques prêtes à l'emploi pour le rendu de HTML à Canvas ne nous convenaient pas avec la vitesse de travail et les fonctionnalités insuffisantes.
Nous avons examiné plusieurs solutions:
- Canvas.fillText standard. Capable de dessiner du texte comme en html, peut être stylisé, fonctionne dans tous les navigateurs. Mais il ne sait pas comment dessiner des liens comme dans un éditeur html des textes multilignes avec une mise en forme différente. Ces difficultés peuvent être résolues, mais nécessitent beaucoup de temps;
- Dessinez DOM sur le dessus de Canvas. L'option ne nous convenait pas, car dans notre produit, chaque objet créé a son propre z-index sur toile. Et le mélanger avec le z-index DOM ne fonctionnera pas.
- Convertissez html en svg. Il est capable de transformer le html en image grâce à l'élément foreignObject. Cela vous permet de faire du html à l'intérieur de svg et de l'utiliser comme image. Nous avons choisi cette option.
Caractéristiques SVG foreignObject
Comment fonctionne SVG foreignObject: nous avons du HTML de l'éditeur → mettre du HTML dans foreignObject → de la magie → obtenir l'image → ajouter l'image au canevas
A propos de la magie. Malgré le fait que la plupart des navigateurs prennent en charge la balise foreignObject, chacun a ses propres caractéristiques pour utiliser le résultat avec le canevas. FireFox fonctionne avec un objet Blob, dans Edge, vous devez faire Base64 pour l'image et renvoyer l'url de données, et dans IE11, la balise ne fonctionne pas du tout.
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 }
Après avoir travaillé avec SVG, nous avons obtenu des bogues intéressants que nous n'avons pas remarqués sur Flash. Le texte avec la même taille et la même police dans différents navigateurs était affiché différemment. Par exemple, le dernier mot d'une ligne peut être encapsulé et exécuté dans le texte ci-dessous. Il était important pour nous que les utilisateurs obtiennent le même type de widgets, quel que soit le navigateur dans lequel ils travaillent. Il n'y avait aucun problème avec Flash à ce sujet, car il est le même partout.

Nous avons résolu ce problème. Premièrement, pour tous les textes sur une seule ligne, ils ont commencé à toujours considérer la largeur indépendamment du navigateur et des données du serveur. Pour la hauteur, la différence reste, mais dans notre cas cela ne dérange pas les utilisateurs.
Deuxièmement, expérimentalement, nous sommes arrivés à la conclusion qu'il était nécessaire d'ajouter des styles CSS inhabituels pour l'éditeur et svg afin de réduire la différence d'affichage entre les navigateurs:
- crénage de police: auto; contrôle le crénage de la police. Plus de détails
- webkit-font-smoothing: anti-crénelage; responsable du lissage. Plus de détails .
Ce que nous avons finalement obtenu grâce au SVG <foreignObject>:
- Nous pouvons dessiner n'importe quel html: texte, tableaux, graphiques
- La balise renvoie une image vectorielle.
- La balise fonctionne dans tous les navigateurs modernes sauf IE11
Pourquoi nous avons abandonné ForeignObject
Tout fonctionnait bien, mais une fois que les concepteurs sont venus vers nous et ont demandé d'ajouter le support des polices pour créer des maquettes.

Nous nous sommes demandé si nous pouvions le faire avec foreignObject. Il s'est avéré qu'il a une fonctionnalité qui, lors de la résolution de ce problème, devient une faille fatale. Il peut afficher du HTML à l'intérieur de lui-même, mais ne peut pas accéder aux ressources externes, donc toutes les ressources avec lesquelles il travaille doivent être converties en base64 et ajoutées à l'intérieur de svg.

Cela signifie que si vous avez quatre textes écrits par OpenSans, vous devez télécharger cette police à l'utilisateur quatre fois. Cette option ne nous convenait pas.
Nous avons décidé d'écrire notre texte de toile avec ... de bonnes performances, la prise en charge des images vectorielles, nous n'oublierons pas IE 11
Pourquoi une image vectorielle est-elle importante pour nous? Dans notre produit, tout objet sur la carte peut être zoomé, et avec une image vectorielle, nous ne pouvons le créer qu'une seule fois et le réutiliser quel que soit le zoom. Canvas.fillText dessine un bitmap: dans ce cas, nous devons redessiner l'image avec chaque zoom, ce qui, comme nous le pensions, affecte grandement les performances.
Créer un prototype
Tout d'abord, nous avons créé un prototype simple pour tester ses performances.

Le principe de fonctionnement du prototype:
- Nous donnons à la fonction «texte»;
- De là, nous obtenons un objet dans lequel il y a chaque mot du texte, avec des coordonnées et des styles pour le rendu;
- Donnez l'objet à Canvas;
- La toile dessine du texte.
Le prototype avait plusieurs tâches: vérifier que le redessin de Canvas avec la mise à l'échelle aura lieu sans délai et que le temps pour transformer le html en objet ne sera plus que de créer une image svg.
Le prototype a fait face à la première tâche, la mise à l'échelle n'a presque pas affecté les performances lors du dessin des textes. Il y avait des problèmes avec la deuxième tâche: le traitement de grandes quantités de texte prend suffisamment de temps et les premières mesures des performances ont donné de mauvais résultats. Pour dessiner du texte à partir de 1K caractères, la nouvelle approche a pris presque 2 fois plus de temps que svg.

Nous avons décidé d'utiliser le moyen le plus fiable pour optimiser le code - «remplacez le test par celui dont nous avons besoin» ;-). Mais sérieusement, nous sommes allés voir les analystes et leur avons demandé combien de temps les textes étaient le plus souvent créés par nos utilisateurs. Il s'est avéré que la taille moyenne du texte est de 14 caractères. Pour de tels textes courts, notre prototype a montré des résultats de performance nettement meilleurs, comme la dépendance de la vitesse sur le volume de texte est linéaire, et l'habillage en svg se fait presque toujours en même temps, quelle que soit la longueur du texte. Cela nous convenait: nous pouvons perdre en performance sur les textes longs, mais dans la plupart des cas notre vitesse sera meilleure que svg.
Après plusieurs itérations de travail sur la mise à jour de Canvas Text, nous avons obtenu l'algorithme suivant:
Étape 1. Nous nous divisons en blocs logiques- Nous divisons le texte en blocs: paragraphes, listes;
- Nous divisons les blocs en blocs plus petits selon les styles;
- Nous brisons les blocs en mots.
Étape 2. Nous collectons dans un seul objet avec des coordonnées et des styles- Comptez la largeur et la hauteur de chaque mot en px;
- Nous relions les mots divisés, car au point 2, certains mots ont été divisés en plusieurs;
- À partir des mots, nous collectons les lignes, si le mot ne rentre pas dans la ligne, nous coupons jusqu'à ce qu'il rentre;
- Nous collectons des paragraphes et des listes;
- Nous calculons x, y pour chaque mot;
- Nous obtenons un objet prêt à l'emploi pour le rendu.
L'avantage de cette approche est que nous pouvons couvrir tout le code du HTML vers un objet texte avec des tests unitaires. Grâce à cela, nous pouvons vérifier séparément le rendu et l'analyse elle-même, ce qui nous a permis d'accélérer considérablement le développement.
En conséquence, nous avons pris en charge les polices et IE 11, couvert tout avec des tests unitaires, et la vitesse de rendu dans la plupart des cas est devenue plus élevée que celle de foreignObject. Enregistré dans les utilisateurs bêta et publié. Le succès semble être!
Le succès a duré 30 minutes
Jusqu'à présent, les gars avec un système d'écriture pour droitiers n'ont pas écrit de support technique. Il s'est avéré que nous avons oublié l'existence de ces langues:

Heureusement, l'ajout de la prise en charge du système d'écriture pour droitiers n'a pas été difficile, car le Canvas.fillText standard le prend déjà en charge.
Mais pendant que nous faisions face à cela, nous sommes tombés sur des cas encore plus intéressants que fillText ne pouvait plus prendre en charge. Nous sommes tombés sur des textes bidirectionnels dans lesquels une partie du texte est écrite de droite à gauche, puis de gauche à droite et de nouveau de droite à gauche.

La seule solution que nous connaissions était d'entrer dans la spécification W3C pour les navigateurs et d'essayer de répéter cela dans Canvas Text. C'était difficile et douloureux, mais nous avons pu ajouter un support de base. Plus sur bidirectionnel:
un et
deux .
Brèves conclusions que nous avons faites pour nous-mêmes
- Pour afficher le HTML dans une image, utilisez SVG foreignObject;
- Analysez toujours votre produit pour la prise de décision;
- Faites des prototypes. Ils peuvent montrer que des décisions complexes ne peuvent le paraître qu'à première vue;
- Écrivez le code immédiatement afin qu'il puisse être couvert par des tests;
- Dans un produit international, il est important de ne pas oublier qu'il existe de nombreuses langues différentes, dont le bidérectionnel.
Si vous avez de l'expérience dans la résolution de tels problèmes, partagez-la dans les commentaires.