从 Javascript 中的 SVG 对象中提取所有行
Extract all lines from an SVG object in Javascript
我试图从一个 SVG 对象中提取所有的线条,以便它们可以被一个特殊的光栅化器渲染,但事实证明它比我想象的要复杂得多。
主要挑战是:
svg <line>
和 <polyline>
元素有时可以包含在由元素呈现的 <defs>
组中。这些需要展平。
<path>
元素也需要转换为行元素。我很乐意扔掉花哨的曲线,坚持使用直线。
有没有一种简单的方法可以做到这一点,或者有一些已经存在的代码可以避免重新发明轮子?我在 Javascript.
工作
谢谢!
第二点在先:
将弯曲路径转换为直线路径:
此时,它不会将其转换为 <line>
个元素,但这样做应该不会太困难,我不确定您是否真的需要这个。
function simplifyPathes(svg) {
var pathes = svg.querySelectorAll('path');
for (i = 0; i < pathes.length; i++) {
for (var j = 0; j < pathes[i].pathSegList.numberOfItems; j++) {
var segment = pathes[i].pathSegList.getItem(j);
if (segment.pathSegType > 4) {
if (segment.pathSegType & 1) {
var newSeg = pathes[i].createSVGPathSegLinetoRel(segment.x, segment.y);
} else {
var newSeg = pathes[i].createSVGPathSegLinetoAbs(segment.x, segment.y);
}
pathes[i].pathSegList.replaceItem(newSeg, j);
}
}
}
}
setTimeout(function(){simplifyPathes(document.querySelector('svg'))}, 1500);
<svg width="600" height="300" viewBox="0 0 600 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="path" fill="none" stroke="#000000" stroke-miterlimit="10" d="M286,81c-70.7,14-134.3-57.3-91.3-2.7s145,74.7,53,74.3 C155.7,152.3,150,16,164.3,93c14.3,77-33.7,68.7,53,87s-23.7,31.7-123,6.3-52,86-150,0" />
</defs>
<path fill="none" stroke="#000000" stroke-miterlimit="10" d="M161,88.7c-3.3,44-58.4-5.7-58.4-5.7s-41-31,49.4-31.7 c90.3-0.7,86-61.3,83.7,2.7c-2.3,64,54.3,57.3-19.3,71.6S-27,47.7,89,110.7s107.7,152.7,106.7,65.7" />
<use xlink:href="#path" transform="translate(75, 25)" />
</svg>
可能有更好的方法,但我还没有。
Some readings here
你可能想将它与一些库结合起来,比如 simplify.js。
现在,问题的第一部分。
我假设如果 <use>
个元素有问题,那是因为 Gecko's restrictive rules on external resources in svg Images.
如果不是这样,我很好奇你为什么需要 "flattening"。
我写了一个可怕的 hack,可能会告诉你一个解决方法。
function cloneUses(svg) {
var uses = svg.querySelectorAll('use')
for (i = 0; i < uses.length; i++) {
var elem = uses[i],
id = elem.getAttributeNS('http://www.w3.org/1999/xlink', 'href').substring(1),
c = svg.getElementById(id).cloneNode(true),
b = elem.transform.baseVal,
a = elem.attributes;
if(b.length>1){
b.consolidate();
c.transform.baseVal.appendItem(b.getItem(0));
c.transform.baseVal.consolidate();
}
for (j = 0; j < a.length; j++) {
if (a[j].value !== ('#' + id) && a[j].value !== "transform") {
c.setAttribute(a[j].name, a[j].value);
}
}
elem.parentNode.insertBefore(c, elem);
elem.parentNode.removeChild(elem);
}
}
setTimeout(function(){cloneUses(document.querySelector('svg'))}, 1500);
<svg width="600" height="300" viewBox="0 0 600 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<line id="line" stroke="black" x1="72" y1="120" x2="25" y2="25" />
<path id="path" fill="none" stroke="#000000" stroke-miterlimit="10" d="M286,81c-70.7,14-134.3-57.3-91.3-2.7s145,74.7,53,74.3 C155.7,152.3,150,16,164.3,93c14.3,77-33.7,68.7,53,87s-23.7,31.7-123,6.3-52,86-150,0" />
</defs>
<use xlink:href="#path" transform="translate(75, 25)" />
<use xlink:href="#line" transform="translate(125, 0)" />
</svg>
通过浏览器的检查器查看 svg
Ps:我让你自己想办法把<circle>
变成<line>
。
感谢凯多的回答!非常感激!这样做的原因是我需要为 SVG 使用自定义光栅化器来纠正某些图像失真。它不用于传统的网络环境。
下面的方法使用两个函数将路径分解为线段。一个函数 _samplePath
可以沿任意 SVG 路径采样点。另一个
pathToLines
能够检查每条路径并生成(对于某些路径更有效)所需的线段。对于包含需要大量时间支持的 besiers、arcs 和其他奇怪的路径,pathToLines
返回到 _samplePath
。
这是基于我在 SO 上找到的一些示例。
/**
* Take an SVG path and use a sampling technique to break it down into line segments.
* @param The path element.
* @param The list of lines to write into. Default is to create a new list and return it.
* @param The number of samples to take. Default is length / 2.
* @return The resulting list of line segment.
*/
function _samplePath(path, lines, samples) {
var length = path.getTotalLength();
lines = lines || []
samples = samples || length / 3;
var segSize = length / samples;
var last_pt = path.getPointAtLength(0);
for (var i = 0; i < samples; ++i) {
// Compute the next sample point.
var pt = path.getPointAtLength(segSize * i);
// If this sample point is over a certain size relative to the segSize, we have hit a m/M and jumped.
// Thus we should not render this bit...
if (Math.pow(last_pt.x - pt.x, 2) + Math.pow(last_pt.y - pt.y, 2) < (segSize*segSize*2))
lines.push({ x1: last_pt.x, y1: last_pt.y, x2: pt.x, y2: pt.y })
// Next line please.
last_pt = pt;
}
// Last line.
var pt = path.getPointAtLength(length);
lines.push({ x1: last_pt.x, y1: last_pt.y, x2: pt.x, y2: pt.y })
// Return the lines.
return lines;
}
/**
* Take any SVG path element and flatten it into a line list in absolute coordinates.
* @param path The SVG path element to flatten and segment.
* @param The list of lines to write into. Default is to create a new list and return it.
* @return The list of line segements.
*/
function pathToLines(path, lines) {
// Init.
var xl, yl, x0, y0, x1, y1, x2, y2, segs = path.pathSegList;
var segs = path.pathSegList;
lines = lines || [];
if (segs.length <= 1) return lines;
// Rendering functions.
function emitLine(x, y) { lines.push({ x1: xl, y1: yl, x2: x, y2: y }); xl = x; yl = y; }
//function emitArc(x, y, rx, ry, large, sweep, segmentsize) { }
// If we have weird objects I don't want to support by hand...
// Handle as a complex path with a given number of samples.
for (var i = 0, len = segs.numberOfItems; i < len; i++) {
if (/[csqtaCSQTA]/.test(segs.getItem(i).pathSegTypeAsLetter))
return _samplePath(path, lines);
}
// Otherwise, parse the path for a more accurate and optimal experience.
for (var x = 0, y = 0, i = 0, len = segs.numberOfItems; i < len; i++)
{
var seg = segs.getItem(i);
var c = seg.pathSegTypeAsLetter;
// Handle absolute coordinates.
if (/[MLHVCSQTA]/.test(c)) {
if ('x' in seg) x = seg.x;
if ('y' in seg) y = seg.y;
}
// Handle relative coordinates.
else
{
//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) x += seg.x;
if ('y' in seg) y += seg.y;
}
switch (c)
{
case 'm': case 'M': x0=x, y0=y; xl=x; yl=y; break; // MoveTo (just skip)
case 'l': case 'L': emitLine(x, y); break; // LineTo (emit)
case 'h': case 'H': emitLine(x, yl); break; // LineToHorizontalAbs (emit with y0)
case 'v': case 'V': emitLine(xl, y); break; // LinetoVerticalAbs (emit with x0)
case 'z': case 'Z': emitLine(x0, y0); break; // Move back to zero (close the line).
// Flatten an arc efficiently?
//case 'a': case 'A': console.log("Arc: " + seg); break;
}
}
return lines;
}
与 <use>
和 <defs>
相关的问题的另一部分使用了 SVG 矩阵。
// Get the use element and its target transformation matrix t0.
var element = $("use").get(0);
var link = use[i].getAttribute("xlink:href")
var t0 = use[i].getScreenCTM();
// Find the source element and its current transformation matrix t1.
// This is not the same as the <use> element, because the inners are offset in the <defs>.
var defLink = $(svg).find(link);
var t1 = defLink.get(0).getCTM();
// Then use this function to transform each point on the line by the returned SVGMatrix.
function transformSVGPoint(point, matrix) {
return {
x: point.x * matrix.a + point.y * matrix.c + matrix.e,
y: point.x * matrix.b + point.y * matrix.d + matrix.f
}
}
我试图从一个 SVG 对象中提取所有的线条,以便它们可以被一个特殊的光栅化器渲染,但事实证明它比我想象的要复杂得多。
主要挑战是:
svg
<line>
和<polyline>
元素有时可以包含在由元素呈现的<defs>
组中。这些需要展平。<path>
元素也需要转换为行元素。我很乐意扔掉花哨的曲线,坚持使用直线。
有没有一种简单的方法可以做到这一点,或者有一些已经存在的代码可以避免重新发明轮子?我在 Javascript.
工作谢谢!
第二点在先:
将弯曲路径转换为直线路径:
此时,它不会将其转换为 <line>
个元素,但这样做应该不会太困难,我不确定您是否真的需要这个。
function simplifyPathes(svg) {
var pathes = svg.querySelectorAll('path');
for (i = 0; i < pathes.length; i++) {
for (var j = 0; j < pathes[i].pathSegList.numberOfItems; j++) {
var segment = pathes[i].pathSegList.getItem(j);
if (segment.pathSegType > 4) {
if (segment.pathSegType & 1) {
var newSeg = pathes[i].createSVGPathSegLinetoRel(segment.x, segment.y);
} else {
var newSeg = pathes[i].createSVGPathSegLinetoAbs(segment.x, segment.y);
}
pathes[i].pathSegList.replaceItem(newSeg, j);
}
}
}
}
setTimeout(function(){simplifyPathes(document.querySelector('svg'))}, 1500);
<svg width="600" height="300" viewBox="0 0 600 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<path id="path" fill="none" stroke="#000000" stroke-miterlimit="10" d="M286,81c-70.7,14-134.3-57.3-91.3-2.7s145,74.7,53,74.3 C155.7,152.3,150,16,164.3,93c14.3,77-33.7,68.7,53,87s-23.7,31.7-123,6.3-52,86-150,0" />
</defs>
<path fill="none" stroke="#000000" stroke-miterlimit="10" d="M161,88.7c-3.3,44-58.4-5.7-58.4-5.7s-41-31,49.4-31.7 c90.3-0.7,86-61.3,83.7,2.7c-2.3,64,54.3,57.3-19.3,71.6S-27,47.7,89,110.7s107.7,152.7,106.7,65.7" />
<use xlink:href="#path" transform="translate(75, 25)" />
</svg>
可能有更好的方法,但我还没有。
Some readings here
你可能想将它与一些库结合起来,比如 simplify.js。
现在,问题的第一部分。
我假设如果 <use>
个元素有问题,那是因为 Gecko's restrictive rules on external resources in svg Images.
如果不是这样,我很好奇你为什么需要 "flattening"。
我写了一个可怕的 hack,可能会告诉你一个解决方法。
function cloneUses(svg) {
var uses = svg.querySelectorAll('use')
for (i = 0; i < uses.length; i++) {
var elem = uses[i],
id = elem.getAttributeNS('http://www.w3.org/1999/xlink', 'href').substring(1),
c = svg.getElementById(id).cloneNode(true),
b = elem.transform.baseVal,
a = elem.attributes;
if(b.length>1){
b.consolidate();
c.transform.baseVal.appendItem(b.getItem(0));
c.transform.baseVal.consolidate();
}
for (j = 0; j < a.length; j++) {
if (a[j].value !== ('#' + id) && a[j].value !== "transform") {
c.setAttribute(a[j].name, a[j].value);
}
}
elem.parentNode.insertBefore(c, elem);
elem.parentNode.removeChild(elem);
}
}
setTimeout(function(){cloneUses(document.querySelector('svg'))}, 1500);
<svg width="600" height="300" viewBox="0 0 600 300" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<line id="line" stroke="black" x1="72" y1="120" x2="25" y2="25" />
<path id="path" fill="none" stroke="#000000" stroke-miterlimit="10" d="M286,81c-70.7,14-134.3-57.3-91.3-2.7s145,74.7,53,74.3 C155.7,152.3,150,16,164.3,93c14.3,77-33.7,68.7,53,87s-23.7,31.7-123,6.3-52,86-150,0" />
</defs>
<use xlink:href="#path" transform="translate(75, 25)" />
<use xlink:href="#line" transform="translate(125, 0)" />
</svg>
Ps:我让你自己想办法把<circle>
变成<line>
。
感谢凯多的回答!非常感激!这样做的原因是我需要为 SVG 使用自定义光栅化器来纠正某些图像失真。它不用于传统的网络环境。
下面的方法使用两个函数将路径分解为线段。一个函数 _samplePath
可以沿任意 SVG 路径采样点。另一个
pathToLines
能够检查每条路径并生成(对于某些路径更有效)所需的线段。对于包含需要大量时间支持的 besiers、arcs 和其他奇怪的路径,pathToLines
返回到 _samplePath
。
这是基于我在 SO 上找到的一些示例。
/**
* Take an SVG path and use a sampling technique to break it down into line segments.
* @param The path element.
* @param The list of lines to write into. Default is to create a new list and return it.
* @param The number of samples to take. Default is length / 2.
* @return The resulting list of line segment.
*/
function _samplePath(path, lines, samples) {
var length = path.getTotalLength();
lines = lines || []
samples = samples || length / 3;
var segSize = length / samples;
var last_pt = path.getPointAtLength(0);
for (var i = 0; i < samples; ++i) {
// Compute the next sample point.
var pt = path.getPointAtLength(segSize * i);
// If this sample point is over a certain size relative to the segSize, we have hit a m/M and jumped.
// Thus we should not render this bit...
if (Math.pow(last_pt.x - pt.x, 2) + Math.pow(last_pt.y - pt.y, 2) < (segSize*segSize*2))
lines.push({ x1: last_pt.x, y1: last_pt.y, x2: pt.x, y2: pt.y })
// Next line please.
last_pt = pt;
}
// Last line.
var pt = path.getPointAtLength(length);
lines.push({ x1: last_pt.x, y1: last_pt.y, x2: pt.x, y2: pt.y })
// Return the lines.
return lines;
}
/**
* Take any SVG path element and flatten it into a line list in absolute coordinates.
* @param path The SVG path element to flatten and segment.
* @param The list of lines to write into. Default is to create a new list and return it.
* @return The list of line segements.
*/
function pathToLines(path, lines) {
// Init.
var xl, yl, x0, y0, x1, y1, x2, y2, segs = path.pathSegList;
var segs = path.pathSegList;
lines = lines || [];
if (segs.length <= 1) return lines;
// Rendering functions.
function emitLine(x, y) { lines.push({ x1: xl, y1: yl, x2: x, y2: y }); xl = x; yl = y; }
//function emitArc(x, y, rx, ry, large, sweep, segmentsize) { }
// If we have weird objects I don't want to support by hand...
// Handle as a complex path with a given number of samples.
for (var i = 0, len = segs.numberOfItems; i < len; i++) {
if (/[csqtaCSQTA]/.test(segs.getItem(i).pathSegTypeAsLetter))
return _samplePath(path, lines);
}
// Otherwise, parse the path for a more accurate and optimal experience.
for (var x = 0, y = 0, i = 0, len = segs.numberOfItems; i < len; i++)
{
var seg = segs.getItem(i);
var c = seg.pathSegTypeAsLetter;
// Handle absolute coordinates.
if (/[MLHVCSQTA]/.test(c)) {
if ('x' in seg) x = seg.x;
if ('y' in seg) y = seg.y;
}
// Handle relative coordinates.
else
{
//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) x += seg.x;
if ('y' in seg) y += seg.y;
}
switch (c)
{
case 'm': case 'M': x0=x, y0=y; xl=x; yl=y; break; // MoveTo (just skip)
case 'l': case 'L': emitLine(x, y); break; // LineTo (emit)
case 'h': case 'H': emitLine(x, yl); break; // LineToHorizontalAbs (emit with y0)
case 'v': case 'V': emitLine(xl, y); break; // LinetoVerticalAbs (emit with x0)
case 'z': case 'Z': emitLine(x0, y0); break; // Move back to zero (close the line).
// Flatten an arc efficiently?
//case 'a': case 'A': console.log("Arc: " + seg); break;
}
}
return lines;
}
与 <use>
和 <defs>
相关的问题的另一部分使用了 SVG 矩阵。
// Get the use element and its target transformation matrix t0.
var element = $("use").get(0);
var link = use[i].getAttribute("xlink:href")
var t0 = use[i].getScreenCTM();
// Find the source element and its current transformation matrix t1.
// This is not the same as the <use> element, because the inners are offset in the <defs>.
var defLink = $(svg).find(link);
var t1 = defLink.get(0).getCTM();
// Then use this function to transform each point on the line by the returned SVGMatrix.
function transformSVGPoint(point, matrix) {
return {
x: point.x * matrix.a + point.y * matrix.c + matrix.e,
y: point.x * matrix.b + point.y * matrix.d + matrix.f
}
}