将 SVG 路径转换为相对命令

问题描述 投票:0回答:3

给定一个 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"/>

这个问题是由这个问题引发的。

javascript svg
3个回答
7
投票

Snap.SVG 有

Snap.path.toRelative()

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

小提琴


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 中的所有路径(为了更好的衡量,在同一要点中有一个相应的 svg2abs)。


2
投票

基于 svg 工作草案方法

getPathData()
setPathData()
我想出了德米特里·巴拉诺夫斯基 (Dmitry Baranovskiy) 的波特酒 snap.svg 中使用的 pathToRelative/Absolute 方法。

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/[email protected]/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/[email protected]/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 应用程序。

© www.soinside.com 2019 - 2024. All rights reserved.