最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Convert SVG Path to Relative Commands - Stack Overflow

programmeradmin0浏览0评论

Given an SVG Path element, how can I convert all path commands into relative coordinates? For example, convert this path (which includes every command, absolute and relative, interleaved):

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

into this equivalent path:

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

This question was motivated by this question.

Given an SVG Path element, how can I convert all path commands into relative coordinates? For example, convert this path (which includes every command, absolute and relative, interleaved):

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

into this equivalent path:

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

This question was motivated by this question.

Share Improve this question edited May 23, 2017 at 12:10 CommunityBot 11 silver badge asked Jan 6, 2013 at 4:35 ecmanautecmanaut 5,1502 gold badges47 silver badges67 bronze badges 1
  • 1 One way of doing that non programatically (in case it works for you) is by using the service from this website: lea.verou.me/2019/05/… – Adriel Jr Commented Nov 18, 2021 at 0:28
Add a comment  | 

3 Answers 3

Reset to default 7

Snap.SVG has Snap.path.toRelative().

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

Fiddle

I tweaked Phrogz' convertToAbsolute into this convertToRelative function:

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

Used like so with the path from the question:

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

I also made a little phantomjs shell utility svg2rel which transforms all paths in an svg this way (there's a corresponding svg2abs in the same gist, for good measure).

Based on the svg working draft methods getPathData() and setPathData() I came up with a port of Dmitry Baranovskiy's pathToRelative/Absolute methods used in snap.svg.

getPathData() is intended to replace the deprecated pathSegList methods but is not yet supported by any major browser.
So you need a polyfill like Jarek Foksa's pathdata polyfill..

Example usage:

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

Example snippet based on Lea Verou's post
"Convert SVG path to all-relative or all-absolute commands" (using 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;
}
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>

Fortunately there's no shortage of svg libraries or web apps providing a conversion method.

  • yqnn's "svg-path-editor"
  • thednp's svg path commander
发布评论

评论列表(0)

  1. 暂无评论