2011-03-01 12 views
21

tl; resumen dr: proporcióneme los recursos o ayude a corregir el siguiente código para transformar comandos de ruta para SVG <path> elementos mediante una matriz arbitraria.Horneado se transforma en comandos del elemento de ruta SVG

detalles:
estoy escribiendo una biblioteca para convertir cualquier forma arbitraria SVG en un elemento <path>. Lo tengo funcionando cuando no hay elementos transform="..." en la jerarquía, pero ahora quiero hornear la transformación local del objeto en los propios comandos path data.

Esto está trabajando principalmente (código debajo) cuando se trata de los comandos simples moveto/lineto. Sin embargo, no estoy seguro de la forma adecuada de transformar los controladores Bezier o los parámetros de arcTo.

Por ejemplo, yo soy capaz de convertir este rectángulo redondeado a un <path>:

<rect x="10" y="30" rx="10" ry="20" width="80" height="70" /> 
--> <path d=​"M20,30 L80,30 A10,20,0,0,1,90,50 L90,80 A10,20,0,0,1,80,100 
      L20,100 A10,20,0,0,1,10,80 L10,50 A10,20,0,0,1,20,30" /> 

y me da un resultado válido cuando se transforma sin esquinas redondas:

<rect x="10" y="30" width="80" height="70" 
     transform="translate(-200,0) scale(1.5) rotate(50)" /> 
--> <path d=​"M10,30 L90,30 L90,100 L10,100 L10,30" /> 

Sin embargo, la transformación sólo se los coordenada x/y de los comandos elliptical arc producen resultados divertidos: Rounded rectangle with green blobs oozing from the corners outside the boundary
La línea punteada es th El rect transformado real, el relleno verde es mi camino.

El siguiente es el código que tengo hasta ahora (ligeramente reducido). También tengo un test page donde estoy probando varias formas. Ayúdeme a determinar cómo transformar correctamente el elliptical arc y varios otros comandos de bezier con una matriz de transformación arbitraria.

function flattenToPaths(el,transform,svg){ 
    if (!svg) svg=el; while(svg && svg.tagName!='svg') svg=svg.parentNode; 
    var doc = el.ownerDocument; 
    var svgNS = svg.getAttribute('xmlns'); 

    // Identity transform if nothing passed in 
    if (!transform) transform= svg.createSVGMatrix(); 

    // Calculate local transform matrix for the object 
    var localMatrix = svg.createSVGMatrix(); 
    for (var xs=el.transform.baseVal,i=xs.numberOfItems-1;i>=0;--i){ 
    localMatrix = xs.getItem(i).matrix.multiply(localMatrix); 
    } 
    // Transform the local transform by whatever was recursively passed in 
    transform = transform.multiply(localMatrix); 

    var path = doc.createElementNS(svgNS,'path'); 
    switch(el.tagName){ 
    case 'rect': 
     path.setAttribute('stroke',el.getAttribute('stroke')); 
     var x = el.getAttribute('x')*1,  y = el.getAttribute('y')*1, 
      w = el.getAttribute('width')*1, h = el.getAttribute('height')*1, 
      rx = el.getAttribute('rx')*1, ry = el.getAttribute('ry')*1; 
     if (rx && !el.hasAttribute('ry')) ry=rx; 
     else if (ry && !el.hasAttribute('rx')) rx=ry; 
     if (rx>w/2) rx=w/2; 
     if (ry>h/2) ry=h/2; 
     path.setAttribute('d', 
     'M'+(x+rx)+','+y+ 
     'L'+(x+w-rx)+','+y+ 
     ((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w)+','+(y+ry)) : '') + 
     'L'+(x+w)+','+(y+h-ry)+ 
     ((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+w-rx)+','+(y+h)) : '')+ 
     'L'+(x+rx)+','+(y+h)+ 
     ((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+x+','+(y+h-ry)) : '')+ 
     'L'+x+','+(y+ry)+ 
     ((rx||ry) ? ('A'+rx+','+ry+',0,0,'+(rx*ry<0?0:1)+','+(x+rx)+','+y) : '') 
    ); 
    break; 

    case 'circle': 
     var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1, 
      r = el.getAttribute('r')*1, r0 = r/2+','+r/2; 
     path.setAttribute('d','M'+cx+','+(cy-r)+' A'+r0+',0,0,0,'+cx+','+(cy+r)+' '+r0+',0,0,0,'+cx+','+(cy-r)); 
    break; 

    case 'ellipse': 
     var cx = el.getAttribute('cx')*1, cy = el.getAttribute('cy')*1, 
      rx = el.getAttribute('rx')*1, ry = el.getAttribute('ry')*1; 
     path.setAttribute('d','M'+cx+','+(cy-ry)+' A'+rx+','+ry+',0,0,0,'+cx+','+(cy+ry)+' '+rx+','+ry+',0,0,0,'+cx+','+(cy-ry)); 
    break; 

    case 'line': 
     var x1=el.getAttribute('x1')*1, y1=el.getAttribute('y1')*1, 
      x2=el.getAttribute('x2')*1, y2=el.getAttribute('y2')*1; 
     path.setAttribute('d','M'+x1+','+y1+'L'+x2+','+y2); 
    break; 

    case 'polyline': 
    case 'polygon': 
     for (var i=0,l=[],pts=el.points,len=pts.numberOfItems;i<len;++i){ 
     var p = pts.getItem(i); 
     l[i] = p.x+','+p.y; 
     } 
     path.setAttribute('d',"M"+l.shift()+"L"+l.join(' ') + (el.tagName=='polygon') ? 'z' : ''); 
    break; 

    case 'path': 
     path = el.cloneNode(false); 
    break; 
    } 

    // Convert local space by the transform matrix 
    var x,y; 
    var pt = svg.createSVGPoint(); 
    var setXY = function(x,y,xN,yN){ 
    pt.x = x; pt.y = y; 
    pt = pt.matrixTransform(transform); 
    if (xN) seg[xN] = pt.x; 
    if (yN) seg[yN] = pt.y; 
    }; 

    // Extract rotation and scale from the transform 
    var rotation = Math.atan2(transform.b,transform.d)*180/Math.PI; 
    var sx = Math.sqrt(transform.a*transform.a+transform.c*transform.c); 
    var sy = Math.sqrt(transform.b*transform.b+transform.d*transform.d); 

    // FIXME: Must translate any Horizontal or Vertical lineto commands into absolute moveto 
    for (var segs=path.pathSegList,c=segs.numberOfItems,i=0;i<c;++i){ 
    var seg = segs.getItem(i); 

    // Odd-numbered path segments are all relative 
    // http://www.w3.org/TR/SVG/paths.html#InterfaceSVGPathSeg 
    var isRelative = (seg.pathSegType%2==1); 
    var hasX = seg.x != null; 
    var hasY = seg.y != null; 
    if (hasX) x = isRelative ? x+seg.x : seg.x; 
    if (hasY) y = isRelative ? y+seg.y : seg.y; 
    if (hasX || hasY) setXY(x, y, hasX && 'x', hasY && 'y'); 

    if (seg.x1 != null) setXY(seg.x1, seg.y1, 'x1', 'y1'); 
    if (seg.x2 != null) setXY(seg.x2, seg.y2, 'x2', 'y2'); 
    if (seg.angle != null){ 
     seg.angle += rotation; 
     seg.r1 *= sx; // FIXME; only works for uniform scale 
     seg.r2 *= sy; // FIXME; only works for uniform scale 
    } 
    } 

    return path; 
} 
+1

Para los curiosos, la motivación para esta biblioteca es porque realmente quiero convertir cada objeto en un [polígono de puntos muestreados] (http://phrogz.net/SVG/convert_path_to_polygon.xhtml) para poder realizar [ transformaciones no afines de plano complejo] (http://phrogz.net/SVG/transforming_paths.xhtml) en ellos. – Phrogz

Respuesta

1

Esto es un registro actualizado de cualquier progreso hacia delante que estoy haciendo como una 'respuesta', para ayudar a informar a los demás; si de alguna manera resuelvo el problema yo solo, lo aceptaré.

Actualización 1: Tengo el comando absolute arcto funcionando perfectamente, excepto en los casos de escala no uniforme. Aquí fueron las adiciones:

// Extract rotation and scale from the transform 
var rotation = Math.atan2(transform.b,transform.d)*180/Math.PI; 
var sx = Math.sqrt(transform.a*transform.a+transform.c*transform.c); 
var sy = Math.sqrt(transform.b*transform.b+transform.d*transform.d); 

//inside the processing of segments 
if (seg.angle != null){ 
    seg.angle += rotation; 
    // FIXME; only works for uniform scale 
    seg.r1 *= sx; 
    seg.r2 *= sy; 
} 

Gracias a this answer para un método de extracción simple que estaba usando, y para los cálculos para la extracción de escala no uniforme.

+0

Me gusta tu código simple que el de Raphael. ¿Algún avance en esto? – allenhwkim

+0

@allenhwkim No, no he progresado más de lo que se representa aquí y en mi sitio web. – Phrogz

2

Siempre que traduzca todas las coordenadas a coordenadas absolutas, todas las béziers funcionarán bien; no hay nada mágico sobre sus mangos. En cuanto a los comandos de arco elíptico, la única solución general (manejo de escalado no uniforme, como usted señala, que el comando de arco no puede representar, en el caso general) es primero convertirlos a sus aproximaciones bézier.

https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113 (utiliza absolutizePath en el mismo archivo, un puerto directo de su Convert SVG Path to Absolute Commands truco) hace lo primero, pero aún no la última.

How to best approximate a geometrical arc with a Bezier curve? vincula las matemáticas para convertir arcos en béziers (un segmento de Bézier por segmento de arco 0 < α <= π/2); this paper muestra las ecuaciones al final de la página (su versión más bonita pdf la tiene al final de la sección 3.4.1).

+1

Y si no le importa pararse sobre los hombros de los gigantes, puede, por supuesto, volver a utilizar Dmitry Baranovskiy (licencia MIT) [Raphael.path2curve] (http://raphaeljs.com/reference.html#Raphael.path2curve) en lugar de reimplegándolo usted mismo, así: https://github.com/johan/svg-js-utils/commit/ec55fda7c41a5d19b2d399fdd955b4b5c42ba8ae – ecmanaut

4

Si todos los objetos (círculos, etc.) se convierten primero en rutas, entonces tomar las transformaciones en cuenta es bastante fácil. Hice un banco de pruebas (http://jsbin.com/oqojan/73) donde puede probar la funcionalidad. El banco de pruebas crea comandos de ruta aleatorios y aplica transformaciones aleatorias a las rutas y luego aplana las transformaciones. Por supuesto, en realidad los comandos de ruta y las transformaciones no son aleatorios, pero para la precisión de las pruebas está bien.

hay una función flatten_transformations(), lo que hace la tarea principal:

function flatten_transformations(path_elem, normalize_path, to_relative, dec) { 

    // Rounding coordinates to dec decimals 
    if (dec || dec === 0) { 
     if (dec > 15) dec = 15; 
     else if (dec < 0) dec = 0; 
    } 
    else dec = false; 

    function r(num) { 
     if (dec !== false) return Math.round(num * Math.pow(10, dec))/Math.pow(10, dec); 
     else return num; 
    } 

    // For arc parameter rounding 
    var arc_dec = (dec !== false) ? 6 : false; 
    arc_dec = (dec && dec > 6) ? dec : arc_dec; 

    function ra(num) { 
     if (arc_dec !== false) return Math.round(num * Math.pow(10, arc_dec))/Math.pow(10, arc_dec); 
     else return num; 
    } 

    var arr; 
    //var pathDOM = path_elem.node; 
    var pathDOM = path_elem; 
    var d = pathDOM.getAttribute("d").trim(); 

    // If you want to retain current path commans, set normalize_path to false 
    if (!normalize_path) { // Set to false to prevent possible re-normalization. 
     arr = Raphael.parsePathString(d); // str to array 
     arr = Raphael._pathToAbsolute(arr); // mahvstcsqz -> uppercase 
    } 
    // If you want to modify path data using nonAffine methods, 
    // set normalize_path to true 
    else arr = Raphael.path2curve(d); // mahvstcsqz -> MC 
    var svgDOM = pathDOM.ownerSVGElement; 

    // Get the relation matrix that converts path coordinates 
    // to SVGroot's coordinate space 
    var matrix = pathDOM.getTransformToElement(svgDOM); 

    // The following code can bake transformations 
    // both normalized and non-normalized data 
    // Coordinates have to be Absolute in the following 
    var i = 0, 
     j, m = arr.length, 
     letter = "", 
     x = 0, 
     y = 0, 
     point, newcoords = [], 
     pt = svgDOM.createSVGPoint(), 
     subpath_start = {}; 
    subpath_start.x = ""; 
    subpath_start.y = ""; 
    for (; i < m; i++) { 
     letter = arr[i][0].toUpperCase(); 
     newcoords[i] = []; 
     newcoords[i][0] = arr[i][0]; 

     if (letter == "A") { 
      x = arr[i][6]; 
      y = arr[i][7]; 

      pt.x = arr[i][6]; 
      pt.y = arr[i][7]; 
      newcoords[i] = arc_transform(arr[i][4], arr[i][5], arr[i][6], arr[i][4], arr[i][5], pt, matrix); 
      // rounding arc parameters 
      // x,y are rounded normally 
      // other parameters at least to 5 decimals 
      // because they affect more than x,y rounding 
      newcoords[i][7] = ra(newcoords[i][8]); //rx 
      newcoords[i][9] = ra(newcoords[i][10]); //ry 
      newcoords[i][11] = ra(newcoords[i][12]); //x-axis-rotation 
      newcoords[i][6] = r(newcoords[i][6]); //x 
      newcoords[i][7] = r(newcoords[i][7]); //y 
     } 
     else if (letter != "Z") { 
      // parse other segs than Z and A 
      for (j = 1; j < arr[i].length; j = j + 2) { 
       if (letter == "V") y = arr[i][j]; 
       else if (letter == "H") x = arr[i][j]; 
       else { 
        x = arr[i][j]; 
        y = arr[i][j + 1]; 
       } 
       pt.x = x; 
       pt.y = y; 
       point = pt.matrixTransform(matrix); 
       newcoords[i][j] = r(point.x); 
       newcoords[i][j + 1] = r(point.y); 
      } 
     } 
     if ((letter != "Z" && subpath_start.x == "") || letter == "M") { 
      subpath_start.x = x; 
      subpath_start.y = y; 
     } 
     if (letter == "Z") { 
      x = subpath_start.x; 
      y = subpath_start.y; 
     } 
     if (letter == "V" || letter == "H") newcoords[i][0] = "L"; 
    } 
    if (to_relative) newcoords = Raphael.pathToRelative(newcoords); 
    newcoords = newcoords.flatten().join(" ").replace(/\s*([A-Z])\s*/gi, "$1").replace(/\s*([-])/gi, "$1"); 
    return newcoords; 
} // function flatten_transformations​​​​​ 

// Helper tool to piece together Raphael's paths into strings again 
Array.prototype.flatten || (Array.prototype.flatten = function() { 
    return this.reduce(function(a, b) { 
     return a.concat('function' === typeof b.flatten ? b.flatten() : b); 
    }, []); 
}); 

el código utiliza Raphael.pathToRelative(), Raphael._pathToAbsolute() y Raphael.path2curve(). Raphael.path2curve() es una versión corregida.

Si se llama a flatten_transformations() usando el argumento normalize_path = true, todos los comandos se convierten a Cubics y todo está bien. Y el código puede simplificarse eliminando if (letter == "A") { ... } y eliminando también el manejo de H, V y Z. La versión simplificada puede ser algo así como this.

Pero debido a que alguien puede querer hornear transformaciones y no hacer All Segs -> Cubics normalization, agregué allí una posibilidad a esto. Por lo tanto, si desea aplanar las transformaciones con normalize_path = false, esto significa que los parámetros de arco elíptico también deben aplanarse y no es posible manejarlos simplemente aplicando la matriz a las coordenadas. Dos radis (rx ry), rotación del eje x, bandera de arco grande y bandera de barrido deben manejarse por separado. Por lo tanto, la siguiente función puede aplanar las transformaciones de los Arcos. El parámetro de matriz es una matriz de relación que se usa ya se usa en flatten_transformations().

// Origin: http://devmaster.net/forums/topic/4947-transforming-an-ellipse/ 
function arc_transform(a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint, matrix, svgDOM) { 
    function NEARZERO(B) { 
     if (Math.abs(B) < 0.0000000000000001) return true; 
     else return false; 
    } 

    var rh, rv, rot; 

    var m = []; // matrix representation of transformed ellipse 
    var s, c; // sin and cos helpers (the former offset rotation) 
    var A, B, C; // ellipse implicit equation: 
    var ac, A2, C2; // helpers for angle and halfaxis-extraction. 
    rh = a_rh; 
    rv = a_rv; 

    a_offsetrot = a_offsetrot * (Math.PI/180); // deg->rad 
    rot = a_offsetrot; 

    s = parseFloat(Math.sin(rot)); 
    c = parseFloat(Math.cos(rot)); 

    // build ellipse representation matrix (unit circle transformation). 
    // the 2x2 matrix multiplication with the upper 2x2 of a_mat is inlined. 
    m[0] = matrix.a * +rh * c + matrix.c * rh * s; 
    m[1] = matrix.b * +rh * c + matrix.d * rh * s; 
    m[2] = matrix.a * -rv * s + matrix.c * rv * c; 
    m[3] = matrix.b * -rv * s + matrix.d * rv * c; 

    // to implict equation (centered) 
    A = (m[0] * m[0]) + (m[2] * m[2]); 
    C = (m[1] * m[1]) + (m[3] * m[3]); 
    B = (m[0] * m[1] + m[2] * m[3]) * 2.0; 

    // precalculate distance A to C 
    ac = A - C; 

    // convert implicit equation to angle and halfaxis: 
    if (NEARZERO(B)) { 
     a_offsetrot = 0; 
     A2 = A; 
     C2 = C; 
    } else { 
     if (NEARZERO(ac)) { 
      A2 = A + B * 0.5; 
      C2 = A - B * 0.5; 
      a_offsetrot = Math.PI/4.0; 
     } else { 
      // Precalculate radical: 
      var K = 1 + B * B/(ac * ac); 

      // Clamp (precision issues might need this.. not likely, but better save than sorry) 
      if (K < 0) K = 0; 
      else K = Math.sqrt(K); 

      A2 = 0.5 * (A + C + K * ac); 
      C2 = 0.5 * (A + C - K * ac); 
      a_offsetrot = 0.5 * Math.atan2(B, ac); 
     } 
    } 

    // This can get slightly below zero due to rounding issues. 
    // it's save to clamp to zero in this case (this yields a zero length halfaxis) 
    if (A2 < 0) A2 = 0; 
    else A2 = Math.sqrt(A2); 
    if (C2 < 0) C2 = 0; 
    else C2 = Math.sqrt(C2); 

    // now A2 and C2 are half-axis: 
    if (ac <= 0) { 
     a_rv = A2; 
     a_rh = C2; 
    } else { 
     a_rv = C2; 
     a_rh = A2; 
    } 

    // If the transformation matrix contain a mirror-component 
    // winding order of the ellise needs to be changed. 
    if ((matrix.a * matrix.d) - (matrix.b * matrix.c) < 0) { 
     if (!sweep_flag) sweep_flag = 1; 
     else sweep_flag = 0; 
    } 

    // Finally, transform arc endpoint. This takes care about the 
    // translational part which we ignored at the whole math-showdown above. 
    endpoint = endpoint.matrixTransform(matrix); 

    // Radians back to degrees 
    a_offsetrot = a_offsetrot * 180/Math.PI; 

    var r = ["A", a_rh, a_rv, a_offsetrot, large_arc_flag, sweep_flag, endpoint.x, endpoint.y]; 
    return r; 
} 

Ejemplo OLD:

hice an example que tiene un trazado con segmentos M Q A A Q M, que tiene transformaciones aplicadas. La ruta está dentro de g que también se ha aplicado trans. Y para estar seguro de que esta g está dentro de otra g que tiene diferentes transformaciones aplicadas. Y el código puede:

A) En primer lugar normalizar esos todos los segmentos de trazado (gracias a path2curve de Rafael, a la que hice a bug fix, y después de esta revisión todas las combinaciones de segmento posible camino trabajaron por último:. http://jsbin.com/oqojan/42 El Raphaël originales 2.1.0 tiene comportamiento incorrecto como se puede ver here, si no haga clic en caminos pocas veces para generar nuevas curvas.)

B) Luego aplanar transformaciones usando funciones nativas getTransformToElement(), createSVGPoint() y matrixTransform().

El único que falta es la forma de convertir Círculos, Rectángulos y Polígonos en comandos de ruta, pero hasta donde yo sé, tiene un código excelente para ello.

+0

Esa es una cama de prueba realmente genial, que demuestra otra propiedad del problema: si sus rutas emplean no solo llena sino que también trazos, es aún más complicado aún reproducir los aspectos originales, ya que el trazado contorneado es realmente una forma de contorno con un volumen, derivado de sus propiedades de trazo (ancho, límite de línea y tal vez otros que olvidé). Dada una transformación de inclinación o cizalladura, también ha derivado la ruta del contorno hornee todas las transformaciones en ella, y la renderice sin trazo, y un color de relleno del trazo de la curva original, en la parte superior de su curva de relleno, si tuve uno. – ecmanaut

+0

Pero si te atreves a resolver ese problema también, haz otra respuesta en lugar de retocar esta una tercera vez; con toda probabilidad, la respuesta sería muy difícil de leer, con toda la complejidad, cuando la mayoría de las veces lo que quieres es solo un truco que aplica todas las escalas, rotaciones y traducciones en un buen camino, del cual el anterior hace un gran trabajo. – ecmanaut

+0

En mi camino de prueba, los trazos no se transforman. Si tienen que tener en cuenta, los trazos AFAIK deben convertirse en trazados. Lo mismo se aplica a los textos, trazos de texto y todos los demás objetos que los caminos y sus trazos. Seguramente es posible, casi. Solo las fuentes son difíciles, porque SVG no tiene soporte para el extracto de ruta de cualquier fuente (= fuente de máquina). –

14

he hecho un acoplador en general SVG aplanar.js, que admite todos los comandos de formas y rutas: https://gist.github.com/timo22345/9413158

Uso básico: flatten(document.getElementById('svg'));

Qué hace: Aplana elementos (convierte elementos en caminos y aplana transformaciones). Si el elemento argument (cuyo id está arriba de 'svg') tiene hijos, o sus descendientes tienen hijos, estos elementos secundarios también se aplanan.

Qué se puede aplanar: documento completo SVG, formas individuales (ruta, círculo, elipse, etc.) y grupos. Los grupos anidados se manejan automáticamente.

¿Qué tal atributos? Todos los atributos son copiados. Solo se eliminan los argumentos que no son válidos en el elemento path (por ejemplo, r, rx, ry, cx, cy), pero ya no son necesarios. También se descarta el atributo de transformación, porque las transformaciones se aplanan para los comandos de ruta.

Si desea modificar camino de coordenadas utilizando métodos no afines (por ejemplo, distorsionar la perspectiva.), puede convertir todos los segmentos de curvas cúbicas usando: flatten(document.getElementById('svg'), true);

también hay argumentos 'toAbsolute' (convertir las coordenadas a absoluto) y 'dec', número de dígitos después del separador decimal.

ruta y la forma extrema probador: https://jsfiddle.net/fjm9423q/embedded/result/

ejemplo de uso básica: http://jsfiddle.net/nrjvmqur/embedded/result/

CONS: elemento de texto no está funcionando. Podría ser mi próximo objetivo.

+0

Parece que esta respuesta ya no funciona correctamente con Chrome v50.0.2661.102 en Windows. – Phrogz

+0

Gracias, @Phrogz. Chrome dejó de admitir SVGElement.prototype.getTransformToElement. Actualicé los ejemplos para hacer uso de shim. –

Cuestiones relacionadas