【SVG篇一】SVG基本形状转换成path

15496次浏览

前言

之前文章有介绍过svgo的不错的svg压缩工具,其实,svg基本形状转换成path,也是一种压缩方式,不但可以压缩,而且可以做一些路径动画,路径动画下一篇详细介绍,本文主要介绍一下svg基本形状如何转换成path。

SVG path 路径

svg形状很简单,常见的形状一般如下:

<svg>
   <!-- 矩形-->
   <rect x="60" y="10" rx="10" ry="10" width="30" height="30" fill="blue"></rect>
  </svg>
 <svg>
 <!-- 圆形-->
  <circle cx="100" cy="100" r="50" fill="#000"></circle>
 </svg>
 <svg>
  <ellipse cx="75" cy="75" rx="20" ry="5" fill="#000"/>
 </svg>
 <svg  style="stroke:rgb(255,0,0);stroke-width:2">
  <polyline points="60 110, 65 120, 70 115, 75 130, 80 125, 85 140, 90 135, 95 150, 100 145"/>
 </svg>
 <svg  style="stroke:rgb(255,0,0);stroke-width:2">
  <line x1="10" x2="50" y1="110" y2="150" fill="#000"/>
 </svg>
 <svg  height="210" width="500">
  <polygon points="50 160, 55 180, 70 180, 60 190, 65 205, 50 195, 35 205, 40 190, 30 180, 45 180" style="fill:lime;stroke:purple;stroke-width:1" />
 </svg>

对上面基本形状转换了,那么基本就完成了。

我们介绍一下SVG path的相关命令

如下图:

enter image description here

通常大部分形状,都可以通过指令M(m)、L(l)、H(h)、V(v)、A(a)来实现,注意特别要区分大小写,相对与绝对坐标情况,转换时推荐使用相对路径可减少代码量,例如:

// 以下两个等价
d='M 10 10 20 20'     // (10, 10) (20 20) 都是绝对坐标
d='M 10 10 L 20 20'

// 以下两个等价
d='m 10 10 20 20'     // (10, 10) 绝对坐标, (20 20) 相对坐标
d='M 10 10 l 20 20'

react to path

如下图所示,一个 rect 是由 4 个弧和 4 个线段构成;如果 rect 没有设置 rx 和 ry 则 rect 只是由 4 个线段构成。rect 转换为 path 只需要将 A ~ H 之间的弧和线段依次实现即可。

enter image description here

转换方法如下:

  function rect2path(x, y, width, height, rx, ry) {
          /*
          * rx 和 ry 的规则是:
          * 1. 如果其中一个设置为 0 则圆角不生效
          * 2. 如果有一个没有设置则取值为另一个
          */
          rx = rx || ry || 0;
          ry = ry || rx || 0;
          //非数值单位计算,如当宽度像100%则移除
          if (is NaN(x - y + width - height + rx - ry)) return;
          rx = rx > width / 2 ? width / 2 : rx;
          ry = ry > height / 2 ? height / 2 : ry;
          //如果其中一个设置为 0 则圆角不生效
          if( 0 == rx || 0 == ry){
          // var path =
          // 'M' + x + ' ' + y +
          // 'H' + (x + width) + 不推荐用绝对路径,相对路径节省代码量
          // 'V' + (y + height) +
          // 'H' + x +
          // 'z';
          var path =
          'M' + x + ' ' + y +
          'h' + width +
          'v' + height +
          'h' + -width +
          'z';
} else{
 var path =
    'M' + x + ' ' + (y+ry) +
    'a' + rx + ' ' + ry + ' 0 0 1 ' + rx + ' ' + ( -ry) +
    'h' + (width - rx - rx) +
    'a' + rx + ' ' + ry + ' 0 0 1 ' + rx + ' ' + ry +
    'v' + (height - ry -ry) +
    'a' + rx + ' ' + ry + ' 0 0 1 ' + ( -rx) + ' ' + ry +
    'h' + ( rx + rx -width) +
    'a' + rx + ' ' + ry + ' 0 0 1 ' + ( -rx) + ' ' + ( -ry) +
    'z';
}
   return path;
}

circle/ellipse to path

圆可视为是一种特殊的椭圆,即 rx 与 ry 相等的椭圆,所以可以放在一起讨论。 椭圆可以看成A点到C做180度顺时针画弧、C点到A做180度顺时针画弧即可。

如下:

enter image description here

转换代码如下:

 function ellipse2path(cx, cy, rx, ry) {
    //非数值单位计算,如当宽度像100%则移除
    if (is NaN(cx - cy + rx - ry)) return;
    var path =
    'M' + (cx -rx) + ' ' + cy +
    'a' + rx + ' ' + ry + ' 0 1 0 ' + 2* rx + ' 0' +
    'a' + rx + ' ' + ry + ' 0 1 0 ' + ( -2* rx) + ' 0' +
    'z';
    return path;
}

line to path

function line2path(x1, y1, x2, y2) {
    //非数值单位计算,如当宽度像100%则移除
    if ( isNaN(x1 - y1 + x2 - y2)) return;
    x1 = x1 || 0;
    y1 = y1 || 0;
    x2 = x2 || 0;
    y2 = y2 || 0;
    var path = 'M' + x1 + ' '+ y1 + 'L' + x2 + ' ' + y2;
    return path;
}

polyline/polygon to path

polyline折线、polygon多边形的转换为path比较类似,差别就是polygon多边形会闭合。

 // polygon折线转换
//points = [x1, y1, x2, y2, x3, y3 ...];
function polyline2path (points) {
    var path = 'M' + points.slice( 0, 2).join( ' ') +
    'L' + points.slice( 2).join( ' ');
    return path;
}
// polygon多边形转换
//points = [x1, y1, x2, y2, x3, y3 ...];
function polygon2path (points) {
  var path = 'M' + points.slice( 0, 2).join( ' ') +
  'L' + points.slice( 2).join( ' ') + 'z';
  return path;
}

SVG基本形状转换成path 完整转换代码

下面是封装的一个完整转换代码:

(function () {
    // 是否支持url()函数和基本图形函数的判断
    var isSupportUrl = CSS.supports('offset-path', 'url(#xxx)');
    var isSupportBasicShape = CSS.supports('offset-path', 'circle()');

    window.convertPathData = function (node) {
      // 匹配路径中数值的正则
      var regNumber = /[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?/g;

      if (!node.tagName) return
      var tagName = String(node.tagName).toLowerCase()

      switch (tagName) {
        case 'path':
            var path = node.getAttribute('d');
            break;
        case 'rect':
          var x = Number(node.getAttribute('x'))
          var y = Number(node.getAttribute('y'))
          var width = Number(node.getAttribute('width'))
          var height = Number(node.getAttribute('height'))
          /*
           * rx 和 ry 的规则是:
           * 1. 如果其中一个设置为 0 则圆角不生效
           * 2. 如果有一个没有设置则取值为另一个
           * 3.rx 的最大值为 width 的一半, ry 的最大值为 height 的一半
           */
          var rx = Number(node.getAttribute('rx')) || Number(node.getAttribute('ry')) || 0
          var ry = Number(node.getAttribute('ry')) || Number(node.getAttribute('rx')) || 0

          // 非数值单位计算,如当宽度像100%则移除
          // if (isNaN(x - y + width - height + rx - ry)) return;

          rx = rx > width / 2 ? width / 2 : rx
          ry = ry > height / 2 ? height / 2 : ry

          // 如果其中一个设置为 0 则圆角不生效
          if (rx == 0 || ry == 0) {
            // var path =
            //     'M' + x + ' ' + y +
            //     'H' + (x + width) +
            //     'V' + (y + height) +
            //     'H' + x +
            //     'z';
            var path =
              'M' + x + ' ' + y + 'h' + width + 'v' + height + 'h' + -width + 'z'
          } else {
            var path =
              'M' +
              x +
              ' ' +
              (y + ry) +
              'a' +
              rx +
              ' ' +
              ry +
              ' 0 0 1 ' +
              rx +
              ' ' +
              -ry +
              'h' +
              (width - rx - rx) +
              'a' +
              rx +
              ' ' +
              ry +
              ' 0 0 1 ' +
              rx +
              ' ' +
              ry +
              'v' +
              (height - ry - ry) +
              'a' +
              rx +
              ' ' +
              ry +
              ' 0 0 1 ' +
              -rx +
              ' ' +
              ry +
              'h' +
              (rx + rx - width) +
              'a' +
              rx +
              ' ' +
              ry +
              ' 0 0 1 ' +
              -rx +
              ' ' +
              -ry +
              'z'
          }

          break

        case 'circle':
          var cx = node.getAttribute('cx')
          var cy = node.getAttribute('cy')
          var r = node.getAttribute('r')
          var path =
            'M' +
            (cx - r) +
            ' ' +
            cy +
            'a' +
            r +
            ' ' +
            r +
            ' 0 1 0 ' +
            2 * r +
            ' 0' +
            'a' +
            r +
            ' ' +
            r +
            ' 0 1 0 ' +
            -2 * r +
            ' 0' +
            'z'

          break

        case 'ellipse':
          var cx = node.getAttribute('cx') * 1
          var cy = node.getAttribute('cy') * 1
          var rx = node.getAttribute('rx') * 1
          var ry = node.getAttribute('ry') * 1

          if (isNaN(cx - cy + rx - ry)) return
          var path =
            'M' +
            (cx - rx) +
            ' ' +
            cy +
            'a' +
            rx +
            ' ' +
            ry +
            ' 0 1 0 ' +
            2 * rx +
            ' 0' +
            'a' +
            rx +
            ' ' +
            ry +
            ' 0 1 0 ' +
            -2 * rx +
            ' 0' +
            'z'

          break

        case 'line':
          var x1 = node.getAttribute('x1')
          var y1 = node.getAttribute('y1')
          var x2 = node.getAttribute('x2')
          var y2 = node.getAttribute('y2')
          if (isNaN(x1 - y1 + (x2 - y2))) {
            return
          }

          var path = 'M' + x1 + ' ' + y1 + 'L' + x2 + ' ' + y2

          break

        case 'polygon':
        case 'polyline': // ploygon与polyline是一样的,polygon多边形,polyline折线
          var points = (node.getAttribute('points').match(regNumber) || []).map(
            Number
          )
          if (points.length < 4) {
            return
          }
          var path =
            'M' +
            points.slice(0, 2).join(' ') +
            'L' +
            points.slice(2).join(' ') +
            (tagName === 'polygon' ? 'z' : '')

          break
      }
      return path || '';
    }

    var funAutoInitAndWatching = function () {
        var selector = '[is-offset-path]';

        if (!NodeList.prototype.forEach) {
            NodeList.prototype.forEach = Array.prototype.forEach;
        }

        var funSetOffsetPath = function (node) {
            if (node.nodeType != 1) {
                return;
            }

            var cssVarValueOffsetPath = node.originOffsetPath || window.getComputedStyle(node).getPropertyValue('--offset-path');

            // 如果没有设置--offset-path自定义属性值,则不处理
            if (!cssVarValueOffsetPath) {
                return;
            }

            // 过滤前后的空格
            cssVarValueOffsetPath = cssVarValueOffsetPath.trim();
            // 记住原始的自定义属性值
            node.originOffsetPath = cssVarValueOffsetPath;

            var paramOffsetPath = cssVarValueOffsetPath.replace(/[a-z]+\(([\w\W]*?)\).*/, '$1').replace(/(?:'|")/g, '').replace('\\#', '#');
            // 说明不是函数值语法不处理
            if (/\(/.test(paramOffsetPath)) {
                return;
            }

            var path = '';
            // 如果是url()函数
            if (/^url\(/i.test(cssVarValueOffsetPath)) {
                // 查找匹配的SVG元素
                var eleSvgShape = document.querySelector(paramOffsetPath);

                if (!eleSvgShape || isSupportUrl) {
                    return;
                }

                path = convertPathData(eleSvgShape);
            } else if (!isSupportBasicShape) {
                // 需要元素的尺寸,以便进行百分比值转换成固定的像素值
                // 默认是border-box类型,所以
                var width = node.offsetWidth;
                var height = node.offsetHeight;

                // 如果有设置盒子类型
                var boxSizing = cssVarValueOffsetPath.split(') ')[1];
                if (boxSizing) {
                    boxSizing = boxSizing.trim();
                }
                var objStyle = window.getComputedStyle(node);

                if (boxSizing == 'margin-box') {
                    width = width + (parseFloat(objStyle.marginLeft) || 0) + (parseFloat(objStyle.marginRight) || 0);
                    height = height + (parseFloat(objStyle.marginTop) || 0) + (parseFloat(objStyle.marginBottom) || 0);
                } else if (boxSizing == 'padding-box') {
                    width = node.clientWidth;
                    height = node.clientHeight;
                } else if (boxSizing == 'content-box') {
                    width = node.clientWidth - (parseFloat(objStyle.paddingLeft) || 0) + (parseFloat(objStyle.paddingRight) || 0);
                    height = node.clientHeight - (parseFloat(objStyle.paddingTop) || 0) + (parseFloat(objStyle.paddingBottom) || 0);
                }               

                if (/^inset\(/i.test(cssVarValueOffsetPath)) {
                    // 圆角矩形
                    // 语法:
                    // inset(<length-percentage>{1,4} round <'border-radius'>);
                    // <rect x="10" y="20" width="120" height="90" rx="50" ry="10"></rect>
                    // 语法的圆角部分可能会对应不上
                    var eleRect = document.createElement('rect');
                    var sizeRect = paramOffsetPath.split('round')[0].trim();
                    var sizeRadius = (paramOffsetPath.split('round')[1] || 0).trim();

                    // 一些坐标参数值
                    var x = 0;
                    var y = 0;
                    var rx = 0;
                    var ry = 0;

                    // 1-4个值处理
                    var arrOffset = sizeRect.split(/\s+/);

                    if (arrOffset.length == 0) {
                        arrOffset = [0, 0, 0, 0];
                    } else if (arrOffset.length == 1) {
                        arrOffset = [arrOffset[0], arrOffset[0], arrOffset[0], arrOffset[0]];
                    } else if (arrOffset.length == 2) {
                        arrOffset = [arrOffset[0], arrOffset[1], arrOffset[0], arrOffset[1]];
                    } else if (arrOffset.length == 3) {
                        arrOffset = [arrOffset[0], arrOffset[1], arrOffset[2], arrOffset[1]];
                    }
                    // 百分比值转换成固定的长度值
                    arrOffset = arrOffset.map(function (offset, index) {
                        if (/%$/.test(offset)) {
                            return [height, width, height, width][index] * parseFloat(offset) / 100;
                        }
                        return parseFloat(offset) || 0;
                    });

                    // 圆角的处理
                    var arrRadius = sizeRadius.split('/');

                    rx = (function () {
                        var radius = arrRadius[0];
                        if (/%$/.test(radius)) {
                            return width * parseFloat(radius) / 100;
                        }
                        return parseFloat(radius) || 0;
                    })();
                    ry = (function () {
                        var radius = arrRadius[1];

                        if (!radius) {
                            return cx;
                        }
                        if (/%$/.test(radius)) {
                            return height * parseFloat(radius) / 100;
                        }
                        return parseFloat(radius) || 0;
                    })();

                    // 设置尺寸
                    x = arrOffset[3];
                    y = arrOffset[0];

                    var w = width - arrOffset[3] - arrOffset[1];
                    var h = height - arrOffset[0] - arrOffset[2];

                    eleRect.setAttribute('x', x);
                    eleRect.setAttribute('y', y);
                    eleRect.setAttribute('width', w);
                    eleRect.setAttribute('height', h);
                    eleRect.setAttribute('rx', rx);
                    eleRect.setAttribute('ry', ry);

                    path = convertPathData(eleRect);
                } else if (/^circle\(/i.test(cssVarValueOffsetPath)) {
                    var eleCircle = document.createElement('circle');
                    var cx, cy, r;
                    // 圆语法变成路径语法
                    r = paramOffsetPath.split('at')[0].trim() || '50%';

                    if (/%$/.test(r)) {
                        r = Math.min(width, height) * parseFloat(r) / 100;
                    } else {
                        r = parseFloat(r);
                    }

                    var cxCy = paramOffsetPath.split('at')[1] || '50% 50%';
                    cxCy = cxCy.trim().split(/\s+/);
                    if (cxCy.length == 1) {
                        cxCy = cxCy.push(cxCy[0]);
                    }
                    cxCy = cxCy.map(function (xy, index) {
                        if (/%$/.test(xy)) {
                            return [width, height][index] * parseFloat(xy) / 100;
                        }
                        return parseFloat(xy) || 0;                    
                    });

                    cx = cxCy[0];
                    cy = cxCy[1];

                    eleCircle.setAttribute('cx', cx);
                    eleCircle.setAttribute('cy', cy);
                    eleCircle.setAttribute('r', r);

                    path = convertPathData(eleCircle);
                } else if (/^ellipse\(/i.test(cssVarValueOffsetPath)) {
                    // ellipse( [ <shape-radius>{2} ]? [ at <position> ]? )
                    // <ellipse cx="50" cy="50" rx="40" ry="20"></ellipse>
                    var eleEllipse = document.createElement('ellipse');
                    var cx, cy, rx, ry;
                    // 圆语法变成路径语法
                    rxRy = paramOffsetPath.split('at')[0].trim() || '50% 50%';

                    rxRy = rxRy.split(/\s+/);
                    if (rxRy.length == 1) {
                        rxRy = rxRy.push(rxRy[0]);
                    }
                    rxRy = rxRy.map(function (xy, index) {
                        if (/%$/.test(xy)) {
                            return [width, height][index] * parseFloat(xy) / 100;
                        }
                        return parseFloat(xy) || 0;                    
                    });

                    var cxCy = paramOffsetPath.split('at')[1] || '50% 50%';
                    cxCy = cxCy.trim().split(/\s+/);
                    if (cxCy.length == 1) {
                        cxCy = cxCy.push(cxCy[0]);
                    }
                    cxCy = cxCy.map(function (xy, index) {
                        if (/%$/.test(xy)) {
                            return [width, height][index] * parseFloat(xy) / 100;
                        }
                        return parseFloat(xy) || 0;                    
                    });

                    rx = rxRy[0];
                    ry = rxRy[1];

                    cx = cxCy[0];
                    cy = cxCy[1];



                    eleEllipse.setAttribute('cx', cx);
                    eleEllipse.setAttribute('cy', cy);
                    eleEllipse.setAttribute('rx', rx);
                    eleEllipse.setAttribute('ry', ry);

                    path = convertPathData(eleEllipse);
                } else if (/^polygon\(/i.test(cssVarValueOffsetPath)) {
                    // 多边形的处理
                    var arrPoints = paramOffsetPath.split(/,\s*/);
                    // 变成百分比值变成固定值
                    arrPoints = arrPoints.map(function (strXy) {
                        return strXy.split(/\s+/).map(function (xy, index) {
                            if (/%$/.test(xy)) {
                                return [width, height][index] * parseFloat(xy) / 100;
                            }
                            return parseFloat(xy) || 0;
                        }).join(' ');
                    });

                    path = 'M' + arrPoints.join('L') + 'Z';
                }
            }

            if (path) {
                node.style.setProperty('--offset-path', 'path("'+ path +'")');
            }
        };

        // DOM Insert自动初始化
        if (window.MutationObserver) {
            var observerSelect = new MutationObserver(function (mutationsList) {
                mutationsList.forEach(function (mutation) {
                    var nodeAdded = mutation.addedNodes;

                    if (nodeAdded.length) {
                        nodeAdded.forEach(function (eleAdd) {
                            funSetOffsetPath(eleAdd);
                        });
                    }
                });
            });

            observerSelect.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        // 如果没有开启自动初始化,则返回
        document.querySelectorAll(selector).forEach(function (ele) {
            funSetOffsetPath(ele);
        });
    };

    if (document.readyState != 'loading') {
        funAutoInitAndWatching();
    } else {
        window.addEventListener('DOMContentLoaded', funAutoInitAndWatching);
    }
})();

Tags: svgsvg形状svg路径

相关文章: