javascript 烘焙转换为SVG路径元素命令

nwlqm0z1  于 2023-08-02  发布在  Java
关注(0)|答案(5)|浏览(101)

tl;dr summary:给予我资源或帮助修复下面的代码,通过任意矩阵转换SVG <path>元素的路径命令。
详情

我正在编写一个库,用于将任意SVG形状转换为<path>元素。当层次结构中没有transform="..."元素时,我让它工作,但现在我想将对象的本地转换烘焙到路径数据命令本身。
当处理简单的moveto/lineto命令时,这主要是工作 (代码如下)。但是,我不确定变换贝塞尔控制柄或arcTo参数的适当方法。
例如,我可以将这个圆角矩形转换为<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" />

字符串
在没有任何圆角的情况下进行变换时,我得到了一个有效的结果:

<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" />


但是,仅变换椭圆弧命令的x/y坐标会产生有趣的结果:x1c 0d1x的数据

  • 虚线是实际转换的矩形,绿色填充是我的路径。*

以下是我目前为止的代码(稍微精简)。我也有一个test page,我在那里测试各种形状。请帮助我确定如何正确地转换elliptical arc和其他各种贝塞尔命令给定的任意变换矩阵。

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;
}

hyrbngr7

hyrbngr71#

我已经做了一个通用的SVG拼合器flatten.js,它支持所有的形状和路径命令:https://gist.github.com/timo22345/9413158
基本用法:x1n1x;
它的作用:展平元素(将元素转换为路径并展平变换)。如果参数元素(其id在'svg'之上)有子元素,或者它的后代有子元素,这些子元素也会被扁平化。
什么可以被压扁:整个SVG文档、单个形状(路径、圆、椭圆等)和组。嵌套组将自动处理。
属性呢?复制所有属性。仅丢弃路径元素中无效的参数(例如:r,rx,ry,cx,cy),但是它们不再被需要。此外,“变换”(transform)属性也将被删除,因为变换已展平为路径命令。
如果您想使用非仿射方法修改路径坐标(例如透视扭曲),可以使用以下命令将所有分段转换为三次曲线:flatten(document.getElementById('svg'), true);
还有参数'toAbsolute'(将坐标转换为绝对值)和'dec',小数点分隔符后的位数。
极端路径和形状测试仪:https://jsfiddle.net/fjm9423q/embedded/result/
基本用法示例:http://jsfiddle.net/nrjvmqur/embedded/result/
缺点:文本元素不起作用。这可能是我的下一个目标。

4nkexdtk

4nkexdtk2#

如果每个对象(圆等)都首先转换为路径,那么考虑变换就相当容易了。我做了一个测试平台(http://jsbin.com/oqojan/73),您可以在其中测试功能。测试台创建随机路径命令并将随机变换应用于路径,然后展平变换。当然,在现实中路径命令和变换不是随机的,但为了测试精度,这是好的。
有一个函数flatten_transformations(),它使主要任务:

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);
    }, []);
});

字符串
该代码使用Raphael.pathToRelative()、Raphael._pathToAbsolute()和Raphael.path2curve()。Raphael.path2curve()是一个修复了bug的版本。
如果使用参数normalize_path=true调用flatten_transformations(),则所有命令都转换为Cubics,一切正常。代码可以通过删除if (letter == "A") { ... }以及删除H、V和Z的处理来简化。简化版本可以是类似this的东西。
但是因为有些人可能只想烘焙变换,而不想进行All Segs -> Cubics归一化,所以我添加了一种可能性。因此,如果你想使用normalize_path=false来展平变换,这意味着椭圆弧参数也必须被展平,并且不可能通过简单地将矩阵应用于坐标来处理它们。两个半径(rx ry)、x轴旋转、大圆弧标志和扫掠标志必须分别处理。所以下面的函数可以使弧的变换变平。matrix参数是一个关系矩阵,它来自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;
}


旧示例:
我做了一个an example,它有一个包含M Q A A Q M段的路径,其中应用了转换。路径在g内部,也应用了trans。为了确保这个g是在另一个g的内部,这个g被应用了不同的变换。代码可以:
A)首先规范化所有路径段(感谢Raphaël的path 2curve,我对它做了a bug fix,在此修复之后,所有可能的路径段组合最终都工作了:http://jsbin.com/oqojan/42。原始的Raphaël 2.1.0有错误的行为,如您所见here,如果没有,请单击路径几次以生成新曲线。)
B)然后使用原生函数getTransformToElement()createSVGPoint()matrixTransform()进行扁平化变换。
唯一缺少的是将圆形,矩形和多边形转换为路径命令的方法,但据我所知,你有一个很好的代码。

oknwwptz

oknwwptz3#

  • 这是我作为“答案”所做的任何进展的更新日志,以帮助通知其他人;如果我自己解决了这个问题,我就接受这个。*
    更新1:我已经让绝对arcto命令完美地工作了,除了在非均匀比例的情况下。以下是新增内容:
// 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;
}

字符串
感谢this answer提供了比我使用的更简单的提取方法,以及用于提取非均匀尺度的数学方法。

polhcujo

polhcujo4#

只要你把所有的坐标都转换成绝对坐标,所有的béziers都能正常工作;他们的手柄没有什么神奇之处。至于椭圆弧命令,唯一的通用解决方案(如您所指出的,在一般情况下,处理非均匀缩放,弧命令无法表示)是首先将它们转换为它们的贝塞尔近似值。
https://github.com/johan/svg-js-utils/blob/df605f3e21cc7fcd2d604eb318fb2466fd6d63a7/paths.js#L56..L113 absolutizePath在同一个文件中,你的Convert SVG Path to Absolute Commands黑客的直接端口)做前者,但还没有后者.
How to best approximate a geometrical arc with a Bezier curve?链接用于将圆弧转换为贝塞尔曲线的数学(每个0 < α <= π/2圆弧段一个贝塞尔曲线段); this paper在页面的末尾显示了方程(它更漂亮的pdf版本在3.4.1节的末尾)。

wgxvkvu9

wgxvkvu95#

灵感来自Timo Kähkönen的答案和他的flatten.jsgist
我使用Jarek Foksa's getpathData() polyfill编写了一个类似的帮助脚本来获取所需的数据。

btnConvert.addEventListener('click', () => {
  flattenSVGTransformations(svg)
  output.value = new XMLSerializer().serializeToString(svg)
})

function flattenSVGTransformations(svg) {
  let els = svg.querySelectorAll('text, path, polyline, polygon, line, rect, circle, ellipse');
  els.forEach(el => {
    // convert primitives to paths
    if (el instanceof SVGGeometryElement && el.nodeName !== 'path') {
      let pathData = el.getPathData({
        normalize: true
      });
      let pathNew = document.createElementNS('http://www.w3.org/2000/svg', 'path');
      pathNew.setPathData(pathData);
      copyAttributes(el, pathNew);
      el.replaceWith(pathNew)
      el = pathNew;
    }
    reduceElementTransforms(el);
  });
  // remove group transforms
  let groups = svg.querySelectorAll('g');
  groups.forEach(g => {
    g.removeAttribute('transform');
    g.removeAttribute('transform-origin');
    g.style.removeProperty('transform');
    g.style.removeProperty('transform-origin');
  });
}

function reduceElementTransforms(el, decimals = 3) {
  let parent = el.farthestViewportElement;
  // check elements transformations
  let matrix = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());
  let {
    a,
    b,
    c,
    d,
    e,
    f
  } = matrix;
  // round matrix
  [a, b, c, d, e, f] = [a, b, c, d, e, f].map(val => {
    return +val.toFixed(3)
  });
  let matrixStr = [a, b, c, d, e, f].join('');
  let isTransformed = matrixStr !== "100100" ? true : false;
  if (isTransformed) {
    // matrix to readable transfomr functions
    let transObj = qrDecomposeMatrix(matrix);
    // scale stroke-width
    let scale = (transObj.scaleX + transObj.scaleY) / 2;
    scaleStrokeWidth(el, scale)
    // if text element: consolidate all applied transforms 
    if (el instanceof SVGGeometryElement === false) {
      if (isTransformed) {
        el.setAttribute('transform', transObj.svgTransform);
        el.removeAttribute('transform-origin');
        el.style.removeProperty('transform');
        el.style.removeProperty('transform-origin');
      }
      return false
    }
    /**
     * is geometry elements: 
     * recalculate pathdata
     * according to transforms
     * by matrix transform
     */
    let pathData = el.getPathData({
      normalize: true
    });
    let svg = el.closest("svg");
    pathData.forEach((com, i) => {
      let values = com.values;
      for (let v = 0; v < values.length - 1; v += 2) {
        let [x, y] = [values[v], values[v + 1]];
        let pt = svg.createSVGPoint();
        pt.x = x;
        pt.y = y;
        let pTrans = pt.matrixTransform(matrix);
        // update coordinates in pathdata array
        pathData[i]["values"][v] = +(pTrans.x).toFixed(decimals);
        pathData[i]["values"][v + 1] = +(pTrans.y).toFixed(decimals);
      }
    });
    // apply pathdata - remove transform
    el.setPathData(pathData);
    el.removeAttribute('transform');
    el.style.removeProperty('transform');
    return pathData;
  }
}

function scaleStrokeWidth(el, scale) {
  let styles = window.getComputedStyle(el);
  let strokeWidth = styles.getPropertyValue('stroke-width');
  let stroke = styles.getPropertyValue('stroke');
  strokeWidth = stroke != 'none' ? parseFloat(strokeWidth) * scale : 0;
  // exclude text elements, since they remain transformed
  if (strokeWidth && el.nodeName.toLowerCase() !== 'text') {
    el.setAttribute('stroke-width', strokeWidth);
    el.style.removeProperty('stroke-width');
  }
}
/**
 * get element transforms
 */
function getElementTransform(el, parent, precision = 6) {
  let matrix = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());
  let matrixVals = [matrix.a, matrix.b, matrix.c, matrix.d, matrix.e, matrix.f].map(val => {
    return +val.toFixed(precision)
  });
  return matrixVals;
}
/**
 * copy attributes:
 * used for primitive to path conversions
 */
function copyAttributes(el, newEl) {
  let atts = [...el.attributes];
  let excludedAtts = ['d', 'x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx',
    'ry', 'points', 'height', 'width'
  ];
  for (let a = 0; a < atts.length; a++) {
    let att = atts[a];
    if (excludedAtts.indexOf(att.nodeName) === -1) {
      let attrName = att.nodeName;
      let attrValue = att.nodeValue;
      newEl.setAttribute(attrName, attrValue + '');
    }
  }
}
/**
 *  Decompose matrix to readable transform properties 
 *  translate() rotate() scale() etc.
 *  based on @AndreaBogazzi's answer
 *  https://stackoverflow.com/questions/5107134/find-the-rotation-and-skew-of-a-matrix-transformation#32125700
 *  return object with seperate transform properties 
 *  and ready to use css or svg attribute strings
 */
function qrDecomposeMatrix(matrix, precision = 3) {
  let {
    a,
    b,
    c,
    d,
    e,
    f
  } = matrix;
  // matrix is array
  if (Array.isArray(matrix)) {
    [a, b, c, d, e, f] = matrix;
  }
  let angle = Math.atan2(b, a),
    denom = Math.pow(a, 2) + Math.pow(b, 2),
    scaleX = Math.sqrt(denom),
    scaleY = (a * d - c * b) / scaleX,
    skewX = Math.atan2(a * c + b * d, denom) / (Math.PI / 180),
    translateX = e ? e : 0,
    translateY = f ? f : 0,
    rotate = angle ? angle / (Math.PI / 180) : 0;
  let transObj = {
    translateX: translateX,
    translateY: translateY,
    rotate: rotate,
    scaleX: scaleX,
    scaleY: scaleY,
    skewX: skewX,
    skewY: 0
  };
  let cssTransforms = [];
  let svgTransforms = [];
  for (let prop in transObj) {
    transObj[prop] = +parseFloat(transObj[prop]).toFixed(precision);
    let val = transObj[prop];
    let unit = "";
    if (prop == "rotate" || prop == "skewX") {
      unit = "deg";
    }
    if (prop.indexOf("translate") != -1) {
      unit = "px";
    }
    // combine these properties
    let convert = ["scaleX", "scaleY", "translateX", "translateY"];
    if (val !== 0) {
      cssTransforms.push(`${prop}(${val}${unit})`);
    }
    if (convert.indexOf(prop) == -1 && val !== 0) {
      svgTransforms.push(`${prop}(${val})`);
    } else if (prop == "scaleX") {
      svgTransforms.push(
        `scale(${+scaleX.toFixed(precision)} ${+scaleY.toFixed(precision)})`
      );
    } else if (prop == "translateX") {
      svgTransforms.push(
        `translate(${transObj.translateX} ${transObj.translateY})`
      );
    }
  }
  // append css style string to object
  transObj.cssTransform = cssTransforms.join(" ");
  transObj.svgTransform = svgTransforms.join(" ");
  return transObj;
}
svg {
  width: 50%;
  border: 1px solid #ccc;
  overflow: visible
}

textarea {
  width: 100%;
  min-height: 20em;
}
<p><button id="btnConvert">Flatten</button></p>
<svg id="svg" viewBox="0 0 100 100" overflow="visible">
  <rect x="50" y="0" width="80" height="70"rx="5%" ry="5%" fill="green" stroke="#ccc"  stroke-width="5"
      transform="translate(10,0) scale(0.5) rotate(50)" />
  
  <g class="g-class" style="transform:rotate(-5deg) translate(15px, 15px) scale(0.8) skewX(20deg) skewY(10deg)">
    <g id="g-class" transform-origin="20 -10" transform="rotate(-33 50 50) translate(-10 -10) scale(1.5)">
      <g transform="rotate(-33 50 50) translate(-10 -10) scale(1.5)">
        <path fill="#444" class="icon icon-home" id="icon-home" style="transform: translate(38px, 2px);" d="M10.16,20.12h-5.2v13h-3.44v-16.72l-7.72-8.72l-7.72,8.72v16.72h-3.44v-13h-5.24l16.4-17.4Z" />
      </g>
      <text id="textEl" x="10%" y="40%" text-anchor="middle" style="font-family:Georgia;font-size:50%; stroke-width:1.5px; transform:translateX(-20px)" paint-order="stroke" stroke="#ccc">Text</text>
    </g>
  </g>
</svg>
<h3>Output converted</h3>
<textarea id="output" cols="30" rows="10"></textarea>

<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js"></script>

工作原理

  • 将所有svg几何元素转换为<path>。通过调用element.getpathData({normalize:true}),我们还可以获得<rect><circle>等原语的路径数据。

此外,所有命令都转换为绝对命令,只使用立方béziers和linetos。Arctos被转换为立方贝济尔。

  • 获取每个元素的总变换矩阵,该矩阵涉及所有继承的变换(例如来自父组)
let matrix = parent.getScreenCTM().inverse().multiply(el.getScreenCTM());

  • 通过matrixTransform()重新计算所有命令坐标,如下所示

let pt = svg.createSVGPoint(); pt.x = x; pt.y = y; let pTrans = pt.matrixTransform(matrix);

  • 缩放笔划宽度
  • 删除所有变换属性和样式属性

<text>元素?

显然,我们不能将文本元素转换为路径(除非,我们使用的是像opentype.js或fontkit这样的库)。但是我们可以合并所有对文本有影响的转换,并应用一个自包含的转换属性值。
我使用的qrDecomposeMatrix()辅助函数基于AndreaBogazzi的伟大答案:“查找矩阵变换的旋转和倾斜”,将当前矩阵拆分为单独的变换函数,如translate()scale()等。
测试:Codepen example

相关问题