C'est toujours un travail en cours, n'hésitez pas à apporter des modifications qui, selon vous, l'amélioreraient.

Avant de démarrer

Pour comprendre cet article, il est recommandé d'être à l'aise avec JavaScript, Canvas API et DOM API.

C'est encore mieux si vous êtes familiarisé avec SVG .

Bien que ce ne soit pas banal (pour des raisons de sécurité), il est possible de dessiner du contenu DOM (comme du HTML ) dans un élément Canvas . Cet article, qui tire son origine de ce billet (en) de Robert O'Callahan, explique comment le faire de manière sécurisée et sans danger, en respectant la spécification.

Un aperçu

Vous ne pouvez pas simplement dessiner du HTML dans un élément canvas. Ce qu'il est possible de faire en revanche est d'utiliser une image SVG renfermant le contenu que vous voulez restituer. Pour dessiner du contenu HTML, vous utiliserez un élément <foreignObject> contenant le HTML et ensuite dessinerez l'image SVG dans votre élément canvas.

Étape par étape

La seule partie difficile (et c'est sans doute surestimé) est de créer le fichier SVG de votre image. Tout ce dont vous avez besoin est de créer une chaîne de caractères contenant le XML pour le SVG et de construire un Blob composé de ce qui suit :

  1. Le type de media MIME du blob doit être "image/svg+xml".
  2. L'élément <svg>.
  3. Au sein de cet élément, l'élément <foreignObject>.
  4. Le XHTML (bien formé) lui-même, inséré dans le <foreignObject>.

En utilisant un objet URL comme décrit ci-dessus, vous pouvez insérer votre code HTML dans le même bloc de code plutôt que de le charger depuis une source externe. Vous pouvez bien sûr utiliser une source externe si vous préférez, à condition que son origine reste la même que celle du document actuel.

Exemple

HTML

<canvas id="canvas" style="border:2px solid black;" width="200" height="200">
</canvas>

JavaScript

//Edge Blob polyfill https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
if (!HTMLCanvasElement.prototype.toBlob) {
   Object.defineProperty(HTMLCanvasElement.prototype, 'toBlob', {
     value: function (callback, type, quality) {
       var canvas = this;
       setTimeout(function() {
         var binStr = atob( canvas.toDataURL(type, quality).split(',')[1] ),
         len = binStr.length,
         arr = new Uint8Array(len);

         for (var i = 0; i < len; i++ ) {
            arr[i] = binStr.charCodeAt(i);
         }

         callback( new Blob( [arr], {type: type || 'image/png'} ) );
       });
     }
  });
}

var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');

var data = '<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">' +
           '<foreignObject width="100%" height="100%">' +
           '<div xmlns="http://www.w3.org/1999/xhtml" style="font-size:40px">' +
             '<em>I</em> like ' +
             '<span style="color:white; text-shadow:0 0 2px blue;">' +
             'cheese</span>' +
           '</div>' +
           '</foreignObject>' +
           '</svg>';
    
 data = encodeURIComponent(data);

 
var img = new Image();

img.onload = function() {
  ctx.drawImage(img, 0, 0);
  console.log(canvas.toDataURL());
 
  canvas.toBlob(function(blob) {
     var newImg = document.createElement('img'),
     url = URL.createObjectURL(blob);

     newImg.onload = function() {
     // plus besoin de lire le blob de sorte qu'il est révoqué
     URL.revokeObjectURL(url);
   };

   newImg.src = url;
   document.body.appendChild(newImg);
 });
}

img.src = "data:image/svg+xml," + data

L'exemple ci-dessus produira le résultat suivant :

ScreenshotLive sample

La variable data est réglée avec le contenu de l'image SVG (elle-même incluant le code HTML ) que l'on veut dessiner dans l'élément Canvas .

On crée ensuite une nouvel élément  HTML <img> en appelant new Image(); on y ajoute data ; on alloue un objet URL et on dessine l'image dans le contexte en invoquant drawImage() au chargement.

Sécurité

Vous pouvez vous demander en quoi est-ce sécurisé, étant donné que l'on pourrait éventuellement lire des données sensibles depuis cet élément canvas. Voici pourquoi : cette solution repose sur le fait que l'implémentation des images SVG est très restrictive. Les images SVG ne sont pas autorisées à charger des ressources externes, y compris celles qui peuvent apparaître comme provenant du même domaine. Les ressources comme les images matricielles (images JPEG par exemples) ou les <iframe>s doivent être insérées en tant que URI data: (données).

En plus de cela, vous ne pouvez pas insérer de script dans une image SVG et il n'y a donc pas de risque d'accès au DOM depuis d'autres scripts, les éléments DOM des images SVG ne peuvent quant à eux pas recevoir d'événements d'entrée (input), il n'y a donc pas de moyens de charger des informations privilégiées dans un formulaire (comme un chemin absolu dans le fichier de l'élément <input>) puis de le rendre et d'en lire l'information grâce aux pixels.

Les styles des liens visités ne s'appliquent pas aux liens affichés dans les images SVG afin qu'aucune information concernant l'historique ne puisse être déduite, les thèmes natifs ne sont eux aussi pas appliqués dans les images SVG, rendant ainsi plus difficile la détermination de la plateforme de l'utilisateur.

L'élément canvas résultant doit avoir une origine nettoyée, c'est à dire que vous pouvez appeler toBlob(function(blob){…}) pour retourner un blob pour l'élément canvas ou toDataURL() pour retourner une URI data: encodée en Base64.

Incorporation de svg dans canvas

Il est possible d'intégrer des éléments svg dans une balise canvas qui ne sera pas restituée par le navigateur mais qui sera toujours attachée au dom et accessible au JavaScript. Cette méthode fonctionne actuellement sans erreurs d'origines croisées. Cela donne aux développeurs une méthode supplémentaire pour incorporer une feuille de "sprites" qui ne serait pas belle si elle restait comme une image png.

HTML

<canvas id="canvas" style="border:2px solid black;" width="500" height="500">
  <rect id="test" x="50" y="20" width="100" height="100" style="fill:red;stroke-width:3;stroke:rgb(0,0,0)" ></rect>
  <rect id="test2" x="150" y="170" width="100" height="100" style="fill:pink;stroke-width:3;" ></rect>
  <text id="text" fill="red" font-size="45" font-family="Verdana" x="50" y="286">SVG</text>
  <script>
    document.documentElement.addEventListener('keydown',function(evt){
     var KEY = { w:87, a:65, s:83, d:68 };
     var moveSpeed = 10;
     var ele =  document.getElementById('test');
     var y = parseInt(ele.getAttribute('y'),10);
     var x = parseInt(ele.getAttribute('x'),10);
     var width = parseInt(ele.getAttribute('width'),10);
     var height = parseInt(ele.getAttribute('height'),10);
     
     
     Update = false;
     switch (evt.keyCode){
      case KEY.w:
       ctx.clearRect(x-2,y-2,width+4,height+4);
       y -= moveSpeed;
       ele.setAttribute('y',y);
       svgDrawRect(ele);
      break;
      case KEY.s:
       ctx.clearRect(x-2,y-2,width+4,height+4);
       y += moveSpeed;
       ele.setAttribute('y',y);
       svgDrawRect(ele);
      break;
      case KEY.a:
        ctx.clearRect(x-2,y-2,width+4,height+4);
        x = x - moveSpeed;
        ele.setAttribute('x',x);
       svgDrawRect(ele);
      break;
      case KEY.d:
        ctx.clearRect(x-2,y-2,width+4,height+4);
        x = x + moveSpeed;
        ele.setAttribute('x',x);
        svgDrawRect(ele);
      break;
     }
    },false);
    
    document.documentElement.addEventListener('keyup',function(evt){
      Update = true;
    }, false)
  </script>
 </canvas>
<button onclick="console.log(canvas.toDataURL());">To data url</button>

Cette méthode nécessite l'écriture d'un analyseur svg personnalisé pour charger autant de svg que possible en tant que formes de canvas. L'observateur de mutation surveille le contenu svg de la balise canvas et redessine la toile lorsqu'un élément svg est modifié, ce qui correspond à la façon dont svg fonctionne normalement.

JavaScript

var canvas = document.getElementById("canvas");
 var ctx = canvas.getContext("2d");
 var Update = true;
  window.MutationObserver = window.MutationObserver
 || window.WebKitMutationObserver
 || window.MozMutationObserver;

 var id,
 // créer une instance d'observateur (observer)
 observer = new MutationObserver(function(mutations) {
  var canvas = document.getElementById("canvas");
  var ctx = canvas.getContext("2d");
  if(Update){
   ctx.beginPath();
   ctx.clearRect(0,0,canvas.width,canvas.height);

   draw();
  }
  
 }), config = { attributes: true, subtree: true };

 // passer dans l'élément que vous voulez regarder ainsi que les options
 observer.observe(canvas, config);
 // plus tard, vous arrêtez l'observation
 // observer.disconnect();

 function svgDrawRect(element){
  var y = 0, cx = 0, stroke = false;
  if(element.getAttribute('x')){cx = element.getAttribute('x')}
  if(element.getAttribute('y')){y = element.getAttribute('y')}
  if(element.getAttribute('style')){
   var test = element.getAttribute('style').split(";");
   for(var p = 0; p < test.length; p++){
    switch(test[p].split(":")[0].toLowerCase()){
     case "fill":
      ctx.fillStyle = test[p].split(":")[1];
      break;
     case "stroke":
      ctx.strokeStyle=test[p].split(":")[1];
      stroke = true;
    }
   }
  }
  ctx.fillRect(cx,y,element.getAttribute('width'),element.getAttribute('height'));
  if(stroke){
   ctx.strokeRect(cx,y,element.getAttribute('width'),element.getAttribute('height'));
  }
  ctx.stroke();
 }

 function svgDrawText(element){
  ctx.font = element.getAttribute("font-size")+"px "+element.getAttribute("font-family");
  ctx.strokeText(element.textContent,element.getAttribute("x"),element.getAttribute("y"));
 }

 function draw(){
  var xmlDoc = new DOMParser().parseFromString(canvas.outerHTML, "image/svg+xml");
  var x = xmlDoc.documentElement.childNodes;
  for (i = 0; i < x.length ;i++) {
   //html ajouter textnodes (noeuds texte) avant, et après les éléments les ignorent.
   if(x[i].nodeName == "#text" || x[i].nodeName == "script" || x[i].nodeName == "style"){
    continue;
   }
   
   switch(x[i].nodeName){
    case "rect":
     svgDrawRect(x[i]);
     break;
    case "text":
     svgDrawText(x[i]);
     break;
   }
  }
 }


draw();

Ajout d'animations svg à un canvas

Vous ne devez pas inclure la balise svg autour des éléments svg si vous n'en avez pas besoin, car le navigateur va attacher toutes les propriétés associées, ce qui pourrait ralentir le navigateur, surtout si le svg à dessiner est très grand. Il pourrait être souhaitable d'inclure les propriétés svg, une raison pourrait être d'utiliser des événements d'animation svg. Les événements d'animation svg fonctionnent bien pour les jeux de défense de tour, pour faire suivre aux monstres un chemin spécifique, alors le canvas peut être utilisé pour dessiner les tours de tir. La tentation serait de mettre le svg en dehors du canvas et d'afficher false mais c'est plus de travail pour le navigateur.

L'événement d'animation svg suivant met à jour la valeur "x" toutes les quelques secondes. L'événement observateur ne remarquera pas les changements de la valeur "x" puisque le dom n'est mis à jour que lorsque le navigateur a besoin de cette valeur particuliaire. Une solution consiste à utiliser à la place setInterval appelé toutes les quelques millisecondes. Il est également nécessaire de mettre à jour la fonction de dessin avant.

HTML

<canvas id="canvas"  width="500" height="500">
    <svg id="animatedSvg" width="500" height="500" viewPort="0 0 500 500" version="1.1"
     xmlns="http://www.w3.org/2000/svg">
      <rect id="rect" x="10" y="10" width="100" height="100">
        <animate attributeType="XML" attributeName="x" from="-100" to="120"
            dur="10s" repeatCount="indefinite"/>
      </rect>
    </svg>
</canvas>

JavaScript

 

setInterval(function(){
    var canvas = document.getElementById("canvas");
    var ctx = canvas.getContext("2d");

    if(Update){

        ctx.beginPath();

        ctx.clearRect(0,0,canvas.width,canvas.height);
        draw(canvas);
    }
   
},10);

function draw(ele){
    var xmlDoc = new DOMParser().parseFromString(ele.outerHTML, "image/svg+xml");
    var x = xmlDoc.documentElement.childNodes;
    for (i = 0; i < x.length ;i++) {
        //html ajoute des textnodes (noeuds texte) avant, et après les éléments les ignorent.
        if(x[i].nodeName == "#text" || x[i].nodeName == "script" || x[i].nodeName == "style"){ continue; }
        switch(x[i].nodeName)
        {
            case "rect":
                svgDrawRect(x[i]);
                break;
            case "text": svgDrawText(x[i]);
                break;
            case "svg": draw(x[i]);
            break;
        }
    }
}

 

Dessin HTML

Le code SVG devant être du code XML valide, par opposition à la sérialisation html de HTML5, vous devez analyser HTML pour obtenir la sortie bien formée de l'analyseur HTML. Le code suivant est le moyen le plus simple d'analyser le code HTML.

var doc = document.implementation.createHTMLDocument('');
doc.write(html);

// Vous devez donner une valeur au xmlns si vous souhaitez immédiatement normaliser
// en une chaîne de caractères plutôt que de l'ajouter à un
// <foreignObject> dans le DOM
doc.documentElement.setAttribute('xmlns', doc.documentElement.namespaceURI);

// Avoir le balisage bien formé
html = (new XMLSerializer).serializeToString(doc);

Voir aussi

  • Canvas
  • Tutoriel canvas
  • Dessiner du contenu DOM dans un élément canvas (article de blog (en))
  • rasterizeHTML.js, une implémentation liée à cet article
  • Drawing math equations into a canvas (en), utilisant TeXZilla. Le gestionnaire d'événements observateur ne remarquera pas les changements de la valeur "x" puisque la valeur dom est seulement mise à jour dans le navigateur lorsque le navigateur a besoin de cette valeur particulière. La solution consiste à utiliser à la place l'événement setInterval appelé toutes les quelques millisecondes. Il est aussi nécessaire de mettre à jour la fonction de dessin avant.

Étiquettes et contributeurs liés au document

Étiquettes : 
Contributeurs à cette page : loella16, Delapouite, tregagnon, SphinxKnight
Dernière mise à jour par : loella16,