共计 7981 个字符,预计需要花费 20 分钟才能阅读完成。
前言
之前文章有介绍过 svgo 的不错的 svg 压缩工具,其实,svg 基本形状转换成 path,也是一种压缩方式,不但可以压缩,而且可以做一些路径动画,路径动画下一篇详细介绍,本文主要介绍一下 svg 基本形状如何转换成 path。
SVG path 路径
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 {1,4} round );
//
// 语法的圆角部分可能会对应不上
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( [ {2} ]? [at ]? )
//
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);
}
})();
正文完