前言
之前文章有介绍过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的相关命令
如下图:
通常大部分形状,都可以通过指令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 之间的弧和线段依次实现即可。
转换方法如下:
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度顺时针画弧即可。
如下:
转换代码如下:
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);
}
})();