En el artículo anterior analizábamos el formato SVG con lo que ya tenemos nuestras primeras pistas de como parsear una imagen vectorial y dibujarla en un canvas. Por tanto en este artículo vamos a explicar como recorrer el formato SVG con un parser de XML e iremos dibujando cada elemento en el canvas. En la siguientes indicaciones se dará por hecho que el lector tiene conocimientos básicos en HTML y JavaScript.
Lo primero que vamos a hacer es definir una estructura HTML básica sobre la que dibujar:
[code lang="html"]
[/code]
Básicamente disponemos de un documento HTML con un elemento canvas (html5) con el identificador “canvas”. Además le hemos dado estilo añadiéndole un borde negro. La etiqueta BODY tiene definido un método onLoad que hará que se ejecute la función JavaScript draw() al cargarse la pagina. Es en este método draw es donde parsearemos el SVG y dibujaremos en el canvas.
Por tanto pasaremos a explicar cada parte del código de la función draw().
Parser XML
Lo primero que hay que hacer es abrir el SVG y recorrerlo con el DOM XML que soporte nuestro navegador, para ello usamos el siguiente código:
[code lang="javascript"]
if (window.XMLHttpRequest)
{ // code for IE7+, Firefox, Chrome, Opera, Safari
xmlhttp=new XMLHttpRequest();
}
else
{ // code for IE6, IE5
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.open("GET","test.svg",false);
xmlhttp.send();
xmlDoc=xmlhttp.responseXML;
[/code]
Por tanto a partir de ahora trabajaremos con el objeto xmlDoc que dispone de métodos que nos permitirá recorre la estructura XML de forma sencilla.
Elemento CANVAS
El otro elemento importante de nuestra aplicación es el canvas donde dibujaremos. Por tanto buscaremos nuestro canvas a partir del ID(“canvas”) que definimos y de el obtendremos el contexto “2D” que contiene los métodos para poder dibujar sobre el.
[code lang="javascript"]
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
[/code]
Empezamos a recorrer el XML
Como hemos dicho en la estructura SVG disponemos de una única etiqueta SVG, por tanto podemos obtener dicho elemento mediante el método getElementsByTagName que nos dará un array de elemento SVG. Al haber solo uno, solo trabajaremos con la posición 0 del array. Por tanto podremos acceder a todos los métodos y nodos hijos de la etiqueta SVG a través del elemento xmlDoc.getElementsByTagName("svg")[0]. La etiqueta SVG tiene dos atributos que nos interesan a la hora de inicializar el canvas, y son la anchura y la altura. Por tanto, a través de la función setAttribute definiremos al anchura y altura del canvas:
[code lang="javascript"]
canvas.setAttribute('width',xmlDoc.getElementsByTagName("svg")[0].getAttribute("width"));
canvas.setAttribute('height',xmlDoc.getElementsByTagName("svg")[0].getAttribute("height"));
[/code]
Una vez configurada el área de trabajo, pasaremos a recorrer todas las capas G del documento y dibujaremos todas las figuras de cada capa:
[code lang="javascript"]
var layers = xmlDoc.getElementsByTagName("g");
for(var l = 0; l < layers.length; l++){
var cchildn = xmlDoc.getElementsByTagName("g")[l].childNodes.length;
for(var g = 0; g < cchildn; g++){
….
}
}
[/code]
Es decir, buscamos todos lo elemento G y por cada elemento recorremos todos su subelementos. Para eso obtendremos el numero de nodos hijos y analizaremos cada uno de ellos para saber que tipo de nodos son y como actuar en función de ello.
En el artículo anterior comentamos que solo nos íbamos a centrar en dos tipos de elementos o nodos: los RECT y los PATH, por tanto usando la propiedad nodeName comprobaremos que tipo de nodo es cada nodo hijo y en función de ello ejecutaremos una rutina u otra para dibujar el elemento.
Nodo RECT
En caso de que el nodo sea un rectángulo, podemos comprobarlo de la siguiente manera:
[code lang="javascript"]
if(xmlDoc.getElementsByTagName("g")[l].childNodes[g].nodeName == "rect"){
ctx.beginPath();
…
ctx.stroke();
ctx.fill();
}
[/code]
Iniciamos con beginPath para indicar que queremos crear una nueva figura (todos los estilos y demás propiedades definidas a partir de ahora solo afectan a este path). Y terminamos con stroke, que dibuja los trazos definidos en el canvas y fill que los rellena.
Como comentamos en el articulo anterior, tanto RECT como PATH tienen un atributo estilo que define el color de fondo de la figura, color de borde, grosor.. por tanto antes que nada deberemos parsear dicho atributo y aplicar los estilos correspondientes:
[code lang="javascript"]
var rstyle = xmlDoc.getElementsByTagName("g")[l].childNodes[g].getAttribute("style").split(';');
for(var j=0; j < rstyle.length; j++){
if(rstyle[j].split(':')[0] == "fill"){
ctx.fillStyle = rstyle[j].split(':')[1];
}
if(rstyle[j].split(':')[0] == "stroke"){
ctx.strokeStyle = rstyle[j].split(':')[1];
}
if(rstyle[j].split(':')[0] == "stroke-width"){
ctx.lineWidth = rstyle[j].split(':')[1];
}
}
[/code]
Por tanto recogeremos los valores fill,stroke y stroke-width del atributo style y por cada uno de ellos definiremos el estilo del path con fillstyle,strokestyle y linewidth respectivamente.
Ya solo nos quedará recoger los datos del rectángulo y dibujarlo.
[code lang="javascript"]
var rwidth = parseInt(xmlDoc.getElementsByTagName("g")[l].childNodes[g].getAttribute("width"));
var rheight = parseInt(xmlDoc.getElementsByTagName("g")[l].childNodes[g].getAttribute("height"));
var rx = parseInt(xmlDoc.getElementsByTagName("g")[l].childNodes[g].getAttribute("x"));
var ry = parseInt(xmlDoc.getElementsByTagName("g")[l].childNodes[g].getAttribute("y"));
ctx.moveTo(rx,ry);
ctx.lineTo(rx+rwidth,ry);
ctx.lineTo(rx+rwidth,ry+rheight);
ctx.lineTo(rx,ry+rheight);
ctx.lineTo(rx,ry-lw);
[/code]
A pesar de que se pueden dibujar rectángulos de muchas formas, nosotros los dibujaremos mediante cuatro líneas, ya que conocemos la posición del vértice superior izquierdo y la anchura y altura del rectángulo. Además esta aproximación permite integrar la posibilidad de bordes redondeados solo con definir un radio de borde y la función quadraticCurveTo (no confundir con la propiedad "lineJoin =round" que redondea la unión entre líneas, aunque en algún caso, de radios muy pequeños, nos puedan dar el mismo resultado).
Nodo PATH
El nodo PATH al igual que el RECT tiene un atributo estilo que lo recorreremos de igual manera, pero en el caso de PATH, como comentamos en el articulo anterior, la estructura del trazo esta descrita en el atributo d, bajo una estructura conocida. Por tanto deberemos recorrer dicha estructura y dibujar los diferentes segmentos en los que se divide el trazo.
[code lang="javascript"]
var datribute = xmlDoc.getElementsByTagName("g")[l].childNodes[g].getAttribute("d");
var datributedata = ""+datribute.replace(/\,/g,' ')+"";
var darray = datributedata.split(" ");
var arc = 0;
for(var j = 0; j < darray.length; j++){
if(darray[j] == "M"){
ctx.moveTo(darray[j+1],darray[j+2]);
j = j+2;
}
if(darray[j] == "L"){
ctx.lineTo(darray[j+1],darray[j+2]);
j = j+2;
}
if(darray[j] == "C"){
ctx.bezierCurveTo(darray[j+1],darray[j+2],darray[j+3],darray[j+4],darray[j+5],darray[j+6]);
j = j+6;
}
if(darray[j] == "A"){
if(arc == 0){
ctx.scale(1, 1);
ctx.arc(parseInt(darray[j+1]), parseInt(darray[j+2]), parseInt(darray[j+6]/2), 0, Math.PI*2, false);
addJsText("ctx.arc("+parseInt(darray[j+1])+","+parseInt(darray[j+2])+","+parseInt(darray[j+6]/2)+", 0, Math.PI*2, false);");
j = j+7;
ctx.stroke();
ctx.fill();
addJsText("ctx.stroke();");
addJsText("ctx.fill();");
ctx.beginPath();
addJsText("ctx.beginPath();");
arc = arc + 1;
}
}
}
[/code]
Por tanto en el caso de estructuras M realizaremos un moveTo, en el caso de L dibujaremos una línea con lineTo, en el caso de C una curva con bezierCurveTo y en el caso de A un arco con arc (en el caos de A la aproximación está simplificada, pendiente de un mejor análisis)
Ejemplo
A continuación tenemos un ejemplo funcional de lo aquí descrito, donde podéis ver todo el javascript completo y con el que podéis hacer diferentes pruebas. Este script no pretende ser una solución para convertir lo archivos svg a canvas, sino una explicación de como se podría crear una librería para hacerlo o como base en caso de que se quisiera hacer un desarrollo a medida para analizar algún tipo de dibujo vectorial concreto.