将SVG路径转换为相对命令

7

给定一个SVG Path元素,如何将所有路径命令转换为相对坐标?例如,将此路径(包括每个命令,绝对和相对,交错)转换为:

<path d="M3,7 L13,7 m-10,10 l10,0 V27 H23 v10 h10
         C33,43 38,47 43,47 c0,5 5,10 10,10
         S63,67 63,67       s-10,10 10,10
         Q50,50 73,57       q20,-5 0,-10
         T70,40             t0,-15
         A5,5 45 1 0 40,20  a5,5 20 0 1 -10,-10
         Z" />

将其转换为相对应的路径:
<path d="m3,7 l10,0 m-10 10 l10,0 v10 h10 v10 h10
         c0,6 5,10 10,10    c0,5 5,10 10,10
         s10,10 10,10       s-10,10 10,10
         q-23,-27 0,-20     q20,-5 0,-10
         t-3,-7             t0-15
         a5,5 45 1 0 -30,-5 a5,5 20 0 1 -10,-10
         z"/>

这个问题的动机来自这个问题


1
一种非编程方式(如果适用于您)是使用此网站的服务:https://lea.verou.me/2019/05/utility-convert-svg-path-to-all-relative-or-all-absolute-commands/ - Adriel Jr
3个回答

7

我将Phrogz的convertToAbsolute函数改进为这个convertToRelative函数:

function convertToRelative(path) {
  function set(type) {
    var args = [].slice.call(arguments, 1)
      , rcmd = 'createSVGPathSeg'+ type +'Rel'
      , rseg = path[rcmd].apply(path, args);
    segs.replaceItem(rseg, i);
  }
  var dx, dy, x0, y0, x1, y1, x2, y2, segs = path.pathSegList;
  for (var x = 0, y = 0, i = 0, len = segs.numberOfItems; i < len; i++) {
    var seg = segs.getItem(i)
      , c   = seg.pathSegTypeAsLetter;
    if (/[MLHVCSQTAZz]/.test(c)) {
      if ('x1' in seg) x1 = seg.x1 - x;
      if ('x2' in seg) x2 = seg.x2 - x;
      if ('y1' in seg) y1 = seg.y1 - y;
      if ('y2' in seg) y2 = seg.y2 - y;
      if ('x'  in seg) dx = -x + (x = seg.x);
      if ('y'  in seg) dy = -y + (y = seg.y);
      switch (c) {
        case 'M': set('Moveto',dx,dy);                   break;
        case 'L': set('Lineto',dx,dy);                   break;
        case 'H': set('LinetoHorizontal',dx);            break;
        case 'V': set('LinetoVertical',dy);              break;
        case 'C': set('CurvetoCubic',dx,dy,x1,y1,x2,y2); break;
        case 'S': set('CurvetoCubicSmooth',dx,dy,x2,y2); break;
        case 'Q': set('CurvetoQuadratic',dx,dy,x1,y1);   break;
        case 'T': set('CurvetoQuadraticSmooth',dx,dy);   break;
        case 'A': set('Arc',dx,dy,seg.r1,seg.r2,seg.angle,
                      seg.largeArcFlag,seg.sweepFlag);   break;
        case 'Z': case 'z': x = x0; y = y0; break;
      }
    }
    else {
      if ('x' in seg) x += seg.x;
      if ('y' in seg) y += seg.y;
    }
    // store the start of a subpath
    if (c == 'M' || c == 'm') {
      x0 = x;
      y0 = y;
    }
  }
  path.setAttribute('d', path.getAttribute('d').replace(/Z/g, 'z'));
}

使用问题中给出的路径,可以这样使用:

var path = document.querySelector('path');
convertToRelative(path);
console.log(path.getAttribute('d'));
// m 3 7 l 10 0 m -10 10 l 10 0 v 10 h 10 v 10 h 10 c 0 6 5 10 10 10 c 0 5 5 10 10 10 s 10 10 10 10 s -10 10 10 10 q -23 -27 0 -20 q 20 -5 0 -10 t -3 -7 t 0 -15 a 5 5 45 1 0 -30 -5 a 5 5 20 0 1 -10 -10 z

我还制作了一个小的phantomjs shell实用程序svg2rel,它以这种方式转换svg中的所有路径(同样在相同的gist中有相应的svg2abs,为了保险起见)。


pathSegList现已被弃用:https://dev59.com/1lsX5IYBdhLWcg3wALba - Denilson Sá Maia
1
由于上述API在Chrome(和其他浏览器)中已被弃用,因此此答案不再正确,我正在切换到Snap hack - 或其长兄Rafael中的类似解决方案:http://jsbin.com/voxemav/ - ecmanaut

7

Snap.SVG拥有Snap.path.toRelative()函数。

var rel = Snap.path.toRelative(abspathstring);

Fiddle


值得注意的是:至少此处链接的Snap.SVG版本(http://cdn.jsdelivr.net/snap.svg/0.3.0/snap.svg.js)不会保留您输入的坐标精度,而是将所有坐标四舍五入到三位小数。 - ecmanaut

2

基于svg工作草案中的方法getPathData()setPathData(),我开发了一个snap.svg中使用的Dmitry Baranovskiy路径到相对路径/绝对路径的端口。

getPathData()旨在取代已弃用的pathSegList方法,但尚未得到任何主流浏览器的支持
所以您需要像Jarek Foksa的pathdata polyfill这样的polyfill。

示例用法:

let svg = document.querySelector('svg');
let path = svg.querySelector('path');
let pathData = path.getPathData();
// 2nd argument defines optional rounding: -1 == no rounding; 2 == round to 2 decimals
let pathDataRel = pathDataToRelative(pathData, 3);
path.setPathData(pathDataRel);

基于Lea Verou的文章的示例片段
"将SVG路径转换为全相对或全绝对命令"(使用snap.svg)

/**
 * dependancy: Jarek Foks's pathdata polyfill
 * cdn: https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.4/path-data-polyfill.min.js
 * github: https://github.com/jarek-foksa/path-data-polyfill
 **/

// convert to relative commands
function pathDataToRelative(pathData, decimals = -1) {
    let M = pathData[0].values;
    let x = M[0],
        y = M[1],
        mx = x,
        my = y;
    // loop through commands
    for (let i = 1; i < pathData.length; i++) {
        let cmd = pathData[i];
        let type = cmd.type;
        let typeRel = type.toLowerCase();
        let values = cmd.values;

        // is absolute
        if (type != typeRel) {
            type = typeRel;
            cmd.type = type;
            // check current command types
            switch (typeRel) {
                case "a":
                    values[5] = +(values[5] - x);
                    values[6] = +(values[6] - y);
                    break;
                case "v":
                    values[0] = +(values[0] - y);
                    break;
                case "m":
                    mx = values[0];
                    my = values[1];
                default:
                    // other commands
                    if (values.length) {
                        for (let v = 0; v < values.length; v++) {
                            // even value indices are y coordinates
                            values[v] = values[v] - (v % 2 ? y : x);
                        }
                    }
            }
        }
        // is already relative
        else {
            if (cmd.type == "m") {
                mx = values[0] + x;
                my = values[1] + y;
            }
        }
        let vLen = values.length;
        switch (type) {
            case "z":
                x = mx;
                y = my;
                break;
            case "h":
                x += values[vLen - 1];
                break;
            case "v":
                y += values[vLen - 1];
                break;
            default:
                x += values[vLen - 2];
                y += values[vLen - 1];
        }

        // round coordinates
        if (decimals >= 0) {
            cmd.values = values.map((val) => {
                return +val.toFixed(decimals);
            });
        }
    }
    // round M (starting point)
    if (decimals >= 0) {
        [M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
    }
    return pathData;
}

function pathDataToAbsolute(pathData, decimals = -1) {
    let M = pathData[0].values;
    let x = M[0],
        y = M[1],
        mx = x,
        my = y;
    // loop through commands
    for (let i = 1; i < pathData.length; i++) {
        let cmd = pathData[i];
        let type = cmd.type;
        let typeAbs = type.toUpperCase();
        let values = cmd.values;

        if (type != typeAbs) {
            type = typeAbs;
            cmd.type = type;
            // check current command types
            switch (typeAbs) {
                case "A":
                    values[5] = +(values[5] + x);
                    values[6] = +(values[6] + y);
                    break;

                case "V":
                    values[0] = +(values[0] + y);
                    break;

                case "H":
                    values[0] = +(values[0] + x);
                    break;

                case "M":
                    mx = +values[0] + x;
                    my = +values[1] + y;

                default:
                    // other commands
                    if (values.length) {
                        for (let v = 0; v < values.length; v++) {
                            // even value indices are y coordinates
                            values[v] = values[v] + (v % 2 ? y : x);
                        }
                    }
            }
        }
        // is already absolute
        let vLen = values.length;
        switch (type) {
            case "Z":
                x = +mx;
                y = +my;
                break;
            case "H":
                x = values[0];
                break;
            case "V":
                y = values[0];
                break;
            case "M":
                mx = values[vLen - 2];
                my = values[vLen - 1];

            default:
                x = values[vLen - 2];
                y = values[vLen - 1];
        }

        // round coordinates
        if (decimals >= 0) {
            cmd.values = values.map((val) => {
                return +val.toFixed(decimals);
            });
        }
    }
    // round M (starting point)
    if (decimals >= 0) {
        [M[0], M[1]] = [+M[0].toFixed(decimals), +M[1].toFixed(decimals)];
    }
    return pathData;
}

function roundPathData(pathData, decimals = -1) {
    if (decimals >= 0) {
        pathData.forEach(function (com, c) {
            let values = com["values"];
            values.forEach(function (val, v) {
                pathData[c]["values"][v] = +val.toFixed(decimals);
            });
        });
    }
    return pathData;
}

// reverse pathdata
function reversePathData(pathData) {
    let M = pathData[0];
    let newPathData = [M];
    // split subpaths
    let subPathDataArr = splitSubpaths(pathData);

    subPathDataArr.forEach((subPathData, s) => {
        let subPathDataL = subPathData.length;
        let closed = subPathData[subPathDataL - 1]["type"] == "Z" ? true : false;
        let stripZ = false;

        if (!closed) {
            subPathData.push({
                type: "Z",
                values: []
            });
            subPathDataL++;
            closed = true;
            stripZ = true;
        }

        let subM = subPathData[0]["values"];

        // insert Lineto if last path segment has created by z
        let lastCom = closed
            ? subPathData[subPathDataL - 2]
            : subPathData[subPathDataL - 1];
        let lastComL = lastCom["values"].length;
        let lastXY = [
            lastCom["values"][lastComL - 2],
            lastCom["values"][lastComL - 1]
        ];
        let diff = Math.abs(subM[0] - lastXY[0]);

        if (diff > 1 && closed) {
            subPathData.pop();
            subPathData.push({
                type: "L",
                values: [subM[0], subM[1]]
            });
            subPathData.push({
                type: "Z",
                values: []
            });
        }

        subPathData.forEach(function (com, i) {
            // reverse index
            let subpathDataL = subPathData.length;
            let indexR = subpathDataL - 1 - i;
            let comR = subPathData[indexR];
            let comF = subPathData[i];
            let [typeR, valuesR] = [comR["type"], comR["values"]];
            let [typeF, valuesF] = [comF["type"], comF["values"]];
            if (typeF == "M" && s > 0) {
                newPathData.push(comF);
            } else if (typeR != "M" && typeR != "Z") {
                indexR--;
                let prevCom =
                    i > 0 ? subPathData[indexR] : subPathData[subpathDataL - 1 - i];
                let prevVals = prevCom
                    ? prevCom["values"]
                        ? prevCom["values"]
                        : [0, 0]
                    : [];
                prevVals = prevCom["values"];
                let prevValsL = prevVals.length;
                let newCoords = [];

                if (typeR == "C") {
                    newCoords = [
                        valuesR[2],
                        valuesR[3],
                        valuesR[0],
                        valuesR[1],
                        prevVals[prevValsL - 2],
                        prevVals[prevValsL - 1]
                    ];

                    if (!closed) {
                        let nextVals =
                            i < subpathDataL - 1 ? subPathData[i + 1]["values"] : lastXY;
                        let lastCX = i < subpathDataL - 2 ? nextVals[prevValsL - 2] : subM[0];
                        let lastCY = i < subpathDataL - 2 ? nextVals[prevValsL - 1] : subM[1];
                        newCoords[4] = lastCX;
                        newCoords[5] = lastCY;
                    }
                } else {
                    newCoords = [prevVals[prevValsL - 2], prevVals[prevValsL - 1]];
                }
                newPathData.push({
                    type: typeR,
                    values: newCoords
                });
            }
        });
        if (closed) {
            newPathData.push({
                type: "Z",
                values: []
            });
        }

        //fix M
        if (diff > 1 && stripZ) {
            let firstL = newPathData[1]["values"];
            newPathData[1] = {
                type: "M",
                values: [firstL[0], firstL[1]]
            };
            newPathData.shift();
            newPathData.pop();
        }
    });
    return newPathData;
}

function splitSubpaths(pathData) {
    let pathDataL = pathData.length;
    let subPathArr = [];
    let subPathMindex = [];
    pathData.forEach(function (com, i) {
        let [type, values] = [com["type"], com["values"]];
        if (type == "M") {
            subPathMindex.push(i);
        }
    });
    //split segments after M command
    subPathMindex.forEach(function (index, i) {
        let n = subPathMindex[i + 1];
        let thisSeg = pathData.slice(index, n);
        subPathArr.push(thisSeg);
    });
    return subPathArr;
}
body {
    font: 100%/1.5 Helvetica Neue, sans-serif;
    margin: 1em;
}

svg {
    border: 1px solid #ccc;
    max-height: 10em;
}

pre {
    display: inline-block;
    background: #eee;
    margin: 0;
}

section {
    flex: 1;
    display: flex;
    flex-flow: column;
}

textarea {
    display: block;
    font: inherit;
    font-family: Consolas, monospace;
    width: 100%;
    height: 8em;
    margin: 0.1em 0;
    resize: vertical;
}

footer {
    color: gray;
}

footer a {
    color: inherit;
}

@media (min-width: 800px) {
    .flex {
        display: flex;
        width: 100%;
        gap: 1em;
    }

    svg {
        max-width: 40vw;
    }
}
<form>
    <label>Round coordinates (-1 = no rounding)</label>
    <input class="input" type="number" id="precision" min="-1" value="3">
    <div class="flex">
        <section>
            <label>
                Your path: <textarea class="input" id="origPathT">M46.8 34.9 L49.5 43.2 Q46.5 44.2 42.9 44.5 Q39.3 44.8 34.1 44.8 L34.1 44.8 Q43.4 49 43.4 58.1 L43.4 58.1 Q43.4 66 38 71 Q32.6 76 23.3 76 L23.3 76 Q19.7 76 16.6 75 L16.6 75 Q15.4 75.8 14.7 77.15 Q14 78.5 14 79.9 L14 79.9 Q14 84.2 20.9 84.2 L20.9 84.2 L29.3 84.2 Q34.6 84.2 38.7 86.1 Q42.8 88 45.05 91.3 Q47.3 94.6 47.3 98.8 L47.3 98.8 Q47.3 106.5 41 110.65 Q34.7 114.8 22.6 114.8 L22.6 114.8 Q14.1 114.8 9.15 113.05 Q4.2 111.3 2.1 107.8 Q0 104.3 0 98.8 L0 98.8 L8.3 98.8 Q8.3 102 9.5 103.85 Q10.7 105.7 13.8 106.65 Q16.9 107.6 22.6 107.6 L22.6 107.6 Q30.9 107.6 34.45 105.55 Q38 103.5 38 99.4 L38 99.4 Q38 95.7 35.2 93.8 Q32.4 91.9 27.4 91.9 L27.4 91.9 L19.1 91.9 Q12.4 91.9 8.95 89.05 Q5.5 86.2 5.5 81.9 L5.5 81.9 Q5.5 79.3 7 76.9 Q8.5 74.5 11.3 72.6 L11.3 72.6 Q6.7 70.2 4.55 66.65 Q2.4 63.1 2.4 58 L2.4 58 Q2.4 52.7 5.05 48.5 Q7.7 44.3 12.35 41.95 Q17 39.6 22.7 39.6 L22.7 39.6 Q28.9 39.7 33.1 39.15 Q37.3 38.6 40.05 37.65 Q42.8 36.7 46.8 34.9 L46.8 34.9 ZM22.7 46.2 Q17.5 46.2 14.65 49.45 Q11.8 52.7 11.8 58 L11.8 58 Q11.8 63.4 14.7 66.65 Q17.6 69.9 22.9 69.9 L22.9 69.9 Q28.3 69.9 31.15 66.75 Q34 63.6 34 57.9 L34 57.9 Q34 46.2 22.7 46.2 L22.7 46.2 Z </textarea></label>
            <svg width="100%" height="100%">
                <path id="origPath" fill="indianred" d="" />
            </svg>
            <p id="fileSizeOrig" class="fileSize"></p>

        </section>
        <section>
            <label>
                All-relative path: <textarea id="relPathT" readonly></textarea></label>
            <svg width="100%" height="100%">
                <path id="relativePath" fill="yellowgreen" d="" />
            </svg>
            <p id="fileSizeRel" class="fileSize"></p>

        </section>
        <section>
            <label>
                All-absolute path: <textarea id="absPathT" readonly></textarea></label>
            <svg width="100%" height="100%">
                <path id="absolutePath" fill="hsl(180,50%,50%)" d="" />
            </svg>
            <p id="fileSizeAbs" class="fileSize"></p>

        </section>
    </div>

</form>
<footer>
    <p>Convert path commands to relative and absolute coordinates – using <a href="https://github.com/jarek-foksa/path-data-polyfill">path data polyfill by Jarek Foksa.</a></p>
    <p>Forked from original codepen: by <a href="https://codepen.io/leaverou/pen/RmwzKv">Lea Verou</a>
    </p> Described in this article: <a href="https://lea.verou.me/2019/05/utility-convert-svg-path-to-all-relative-or-all-absolute-commands/">Utility: Convert SVG path to all-relative or all-absolute commands </a></p>
</footer>

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

<script>
    window.addEventListener('DOMContentLoaded', evt => {
        let decimals = parseFloat(precision.value);
        let inputs = document.querySelectorAll('.input');
        let svgs = document.querySelectorAll('svg');
        upDateSVG()
        inputs.forEach(input => {
            input.addEventListener('input', evt => {
                upDateSVG()
            })
        })

        function upDateSVG() {
            decimals = parseFloat(precision.value);
            let d = origPathT.value;
            origPath.setAttribute('d', d);
            let pathData = origPath.getPathData();
            let sizeOrig = filesize(d);
            fileSizeOrig.textContent = sizeOrig + ' KB';
            // relative
            let pathDataRel = pathDataToRelative(pathData, decimals);
            relativePath.setPathData(pathDataRel);
            relPathT.value = relativePath.getAttribute('d');
            let sizeRel = filesize(relPathT.value);
            fileSizeRel.textContent = sizeRel + ' KB';
            // absolute
            let pathDataAbs = pathDataToAbsolute(pathData, decimals);
            absolutePath.setPathData(pathDataAbs);
            absPathT.value = absolutePath.getAttribute('d');
            let sizeAbs = filesize(absPathT.value);
            fileSizeAbs.textContent = sizeAbs + ' KB';
            // adjust viewBox
            svgs.forEach(svg => {
                adjustViewBox(svg);
            })
        }
    })
    //adjustViewBox(svg);
    function adjustViewBox(svg) {
        let bb = svg.getBBox();
        let bbVals = [bb.x, bb.y, bb.width, bb.height].map((val) => {
            return +val.toFixed(2);
        });
        let maxBB = Math.max(...bbVals);
        let [x, y, width, height] = bbVals;
        svg.setAttribute("viewBox", [x, y, width, height].join(" "));
    }
    // show file size
    function filesize(str) {
        let size = new Blob([str]).size / Math.pow(1024, 1);
        return +size.toFixed(3);
    }
</script>

幸运的是,有很多SVG库或Web应用程序提供了转换方法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接