gpt4 book ai didi

d3.js - SVG - 将所有形状/基元转换为

转载 作者:行者123 更新时间:2023-12-02 15:01:50 48 4
gpt4 key购买 nike

我正在执行许多 D3.JS 操作,要求我使用 SVG 路径而不是基元/形状(折线、记录等)。

这个问题很普遍,但我想知道是否可以使用 D3 或其他脚本/库将任何 SVG 原语转换为路径。

作为引用,这里有一个针对折线执行此操作的链接:https://gist.github.com/andytlr/9283541

我想对每个原语都这样做。有任何想法吗?这可能吗?

最佳答案

JavaScript 解决方案

您还可以使用 Jarek Foksa's path-data polyfill 转换所有基元:

它的主要目的是解析路径的d属性到命令数组。

getPathData()还可以从任何 SVGGeometryElement 检索pathData包括像 <rect> 这样的原语, <circle> , <ellipse> , <polygon> .

示例 1:转换所有基元

const svgWrp = document.querySelector('.svgWrp');
const svg = document.querySelector('svg');
const primitives = svg.querySelectorAll('path, line, polyline, polygon, circle, rect');
const svgMarkup = document.querySelector('#svgMarkup');
svgMarkup.value = new XMLSerializer().serializeToString(svg);

function convertPrimitives(svg, primitives) {
primitives.forEach(function(primitive, i) {
/**
* get normalized path data:
* all coordinates are absolute;
* reduced set of commands: M, L, C, Z
*/
let pathData = primitive.getPathData();

//get all attributes
let attributes = [...primitive.attributes];
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
//exclude attributes not needed for paths
let exclude = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'points', 'height',
'width'
];
setAttributes(path, attributes, exclude);
// set d attribute from rounded pathData
path.setPathData(roundPathData(pathData, 1));
primitive.replaceWith(path);
})
// optional: output new svg markup
let newSvgMarkup = new XMLSerializer().serializeToString(svg);
svgMarkup.value = newSvgMarkup;
}

function roundPathData(pathData, decimals = 3) {
pathData.forEach(function(com, c) {
let values = com['values'];
values.forEach(function(val, v) {
pathData[c]['values'][v] = +val.toFixed(decimals);
})
})
return pathData;
}

function setAttributes(el, attributes, exclude = []) {
attributes.forEach(function(att, a) {
if (exclude.indexOf(att.nodeName) === -1) {
el.setAttribute(att.nodeName, att.nodeValue);
}
})
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>

<p><button type="button" onclick="convertPrimitives(svg, primitives)">Convert Primitives</button></p>
<div class="svgWrp">
<svg id="svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 30">
<polygon id="polygon" fill="#ccc" stroke="green" points="9,22.4 4.1,14 9,5.5 18.8,5.5 23.7,14 18.8,22.4 " />
<polyline id="polyline" fill="none" stroke="red" points="43,22.4 33.3,22.4 28.4,14 33.3,5.5 43,5.5 47.9,14 " />
<rect id="rect" x="57.3" y="5.5" rx="2" ry="2" fill="none" stroke="orange" width="16.9" height="16.9" />
<line id="line" fill="none" stroke="purple" x1="52.6" y1="22.4" x2="52.6" y2="5.5" />
<circle class="circle" data-att="circle" id="circle" fill="none" stroke="magenta" cx="87.4" cy="14" r="8.5" />
<path transform="scale(0.9) translate(110,5)" d="M 10 0 A 10 10 0 1 1 1.34 15 L 10 10 z" fill="red" class="segment segment-1 segment-class" id="segment-01" />
</svg>
</div>
<h3>Svg markup</h3>
<textarea name="svgMarkup" id="svgMarkup" style="width:100%; height:20em;"></textarea>

getPathData()还提供标准化方法,将任何元素的几何形状转换为一组精简的绝对命令 - 仅使用:
M , L , C , Z .

二次QT命令也被转换
弧线 A和简写如 VH )

element.getPathData({normalize: true});

示例 2:转换所有基元(标准化)

const svgWrp = document.querySelector('.svgWrp');
const svg = document.querySelector('svg');
const primitives = svg.querySelectorAll('path, line, polyline, polygon, circle, rect');
const svgMarkup = document.querySelector('#svgMarkup');
svgMarkup.value = new XMLSerializer().serializeToString(svg);

function convertPrimitives(svg, primitives) {
primitives.forEach(function(primitive, i) {
/**
* get path data:
*/
let pathData = primitive.getPathData({normalize:true});

//get all attributes
let attributes = [...primitive.attributes];
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
//exclude attributes not needed for paths
let exclude = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'points', 'height',
'width'
];
setAttributes(path, attributes, exclude);
// set d attribute from rounded pathData
path.setPathData(roundPathData(pathData, 1));
primitive.replaceWith(path);
})
// optional: output new svg markup
let newSvgMarkup = new XMLSerializer().serializeToString(svg);
svgMarkup.value = newSvgMarkup;
}

function roundPathData(pathData, decimals = 3) {
pathData.forEach(function(com, c) {
let values = com['values'];
values.forEach(function(val, v) {
pathData[c]['values'][v] = +val.toFixed(decimals);
})
})
return pathData;
}

function setAttributes(el, attributes, exclude = []) {
attributes.forEach(function(att, a) {
if (exclude.indexOf(att.nodeName) === -1) {
el.setAttribute(att.nodeName, att.nodeValue);
}
})
}
<script src="https://cdn.jsdelivr.net/npm/path-data-polyfill@1.0.3/path-data-polyfill.min.js"></script>

<p><button type="button" onclick="convertPrimitives(svg, primitives)">Convert Primitives</button></p>
<div class="svgWrp">
<svg id="svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 150 30">
<polygon id="polygon" fill="#ccc" stroke="green" points="9,22.4 4.1,14 9,5.5 18.8,5.5 23.7,14 18.8,22.4 " />
<polyline id="polyline" fill="none" stroke="red" points="43,22.4 33.3,22.4 28.4,14 33.3,5.5 43,5.5 47.9,14 " />
<rect id="rect" x="57.3" y="5.5" rx="2" ry="2" fill="none" stroke="orange" width="16.9" height="16.9" />
<line id="line" fill="none" stroke="purple" x1="52.6" y1="22.4" x2="52.6" y2="5.5" />
<circle class="circle" data-att="circle" id="circle" fill="none" stroke="magenta" cx="87.4" cy="14" r="8.5" />
<path transform="scale(0.9) translate(110,5)" d="M 10 0 A 10 10 0 1 1 1.34 15 L 10 10 z" fill="red" class="segment segment-1 segment-class" id="segment-01" />
</svg>
</div>
<h3>Svg markup</h3>
<textarea name="svgMarkup" id="svgMarkup" style="width:100%; height:20em;"></textarea>

上面的示例脚本还将保留所有属性,例如 class , id , fill等等

但它会删除像 r 这样的属性, cx , rx特定于基元。

我们需要这个polyfill吗?

不幸的是,getPathData()setPathData()方法仍然是svg 2 drafts/proposals – 旨在替换已弃用的 pathSegList()方法。
希望我们能在不久的将来获得 native 浏览器支持。
由于与更高级的 svg 库(如(snap.svg、d3 等))相比,这个 polyfill 仍然相当轻量(未压缩时约 12.5 KB),因此不会显着增加加载时间。

更新:独立脚本(无polyfill依赖)

这实际上是一个概念证明 - 您可以基于非常基本的值计算来转换 svg 原语 - 无需高级框架/库 - 受到这篇文章的启发:Convert all shapes/primitives into path elements of SVG .

但是当我摆弄自己笨重的转换脚本时,我很快意识到存在一些挑战(Jarek Foksa 的规范化实现完美地解决了),例如:

相对,即基于百分比的单位

<circle cx="50%" cy="50%" r="25%" />  

好的...我想我们需要根据 viewBox 定义的父 svg 边界来计算这些相对于绝对坐标的相对值。属性...也许根本没有可用的 viewBox ...或宽度/高度值。

或者像rx这样的东西, ry属性将圆角边框应用于 <rect>元素 – 为了进行适当的转换,我们需要添加一些曲线命令,例如 a , cs .

路径与基元
确实是 <path>元素可以通过 cubic or quadratic spline commands 绘制基元可以提供的任何形状– 由于它的连接能力(组合多个形状)以及它的相对或速记命令,甚至以更有效的方式。
但它不支持相对单位 - 但是您需要转换的形状可能在很大程度上取决于相对尺寸(例如圆形仪表饼图等)

结论
编写自定义转换脚本并不太难,但要注意一些棘手的细节。

const svg = document.querySelector('svg');
const svgMarkup = document.querySelector('#svgMarkup');
svgMarkup.value = new XMLSerializer().serializeToString(svg);

/**
* example script
**/
function getConvertedMarkup(svg, markupEl, decimals = 1) {
convertPrimitivesNative(svg, decimals);
markupEl.value = new XMLSerializer().serializeToString(svg);
}

/**
* parse svg attributes and convert relative units
**/
function parseSvgAttributes(svg, atts) {
let calcW = 0;
let calcH = 0;
let calcR = 0;

//1. check viewBox
let viewBoxAtt = svg.getAttribute('viewBox');
let viewBox = viewBoxAtt ? viewBoxAtt.split(' ') : [];
[calcW, calcH] = [viewBox[2], viewBox[3]];

//2. check width attributes
if (!calcW || !calcH) {
widthAtt = svg.getAttribute('width') ? parseFloat(svg.getAttribute('width')) : '';
heightAtt = svg.getAttribute('height') ? parseFloat(svg.getAttribute('height')) : '';
[calcW, calcH] = [widthAtt, heightAtt];
}
//3. calculate by getBBox()
if (!calcW || !calcH) {
let bb = svg.getBBox();
[calcW, calcH] = [(calcW ? calcW : bb.width), (calcH ? calcH : bb.height)];
}

// calculate relative radius: needed for non square aspect ratios
calcR = Math.sqrt(Math.pow(calcW, 2) + Math.pow(calcH, 2)) / Math.sqrt(2);

let attArr = [...atts];
let attObj = {};
attArr.forEach(function(att) {
let attName = att.nodeName;
// convert percentages to absolute svg units
let val = att.nodeValue;
let percentAtts = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'r', 'rx', 'ry', 'cx', 'cy', 'width', 'height']
if (val.toString().indexOf('%') !== -1 && percentAtts.indexOf(attName) !== -1) {
// strip units
val = parseFloat(val);
switch (attName) {
case 'cx':
case 'rx':
case 'width':
case 'x':
case 'x1':
case 'x2':
val = 1 / 100 * val * calcW;
break;
case 'cy':
case 'ry':
case 'height':
case 'y':
case 'y1':
case 'y2':
val = 1 / 100 * val * calcH;
break;
case 'r':
val = 1 / 100 * val * calcR;
break;
}
}
attObj[att.nodeName] = val;
});
return attObj;
}

/**
* convert primitive attributes to relative path commands
*/
function convertPrimitivesNative(svg, decimals = 3) {
let primitives = svg.querySelectorAll('line, polyline, polygon, circle, ellipse, rect');

if (primitives.length) {
primitives.forEach(function(primitive) {
let pathData = [];
let type = primitive.nodeName;
let atts = parseSvgAttributes(svg, primitive.attributes, 2);
let path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
//exclude attributes not needed for paths
let exclude = ['x', 'y', 'x1', 'y1', 'x2', 'y2', 'cx', 'cy', 'r', 'rx', 'ry', 'points', 'height',
'width'
];
switch (type) {
case 'rect':
let [rx, ry] = [atts.rx, atts.ry];
rx = !rx && ry ? ry : rx;
ry = !ry && rx ? rx : ry;
let [x, y, width, height] = [atts.x, atts.y, atts.width, atts.height];
let [widthInner, heightInner] = [width - rx * 2, height - ry * 2];
if (rx) {
pathData.push({
type: 'M',
values: [x, (y + ry)]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, rx, -ry]
}, {
type: 'h',
values: [widthInner]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, rx, ry]
}, {
type: 'v',
values: [heightInner]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, -rx, ry]
}, {
type: 'h',
values: [-widthInner]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, -rx, -ry]
}, {
type: 'z',
values: []
});

} else {
pathData.push({
type: 'M',
values: [x, y]
}, {
type: 'h',
values: [width]
}, {
type: 'v',
values: [height]
}, {
type: 'h',
values: [-width]
}, {
type: 'z',
values: []
});
}
break;

case 'line':
let [x1, y1, x2, y2] = [atts.x1, atts.y1, atts.x2, atts.y2];
pathData.push({
type: 'M',
values: [x1, y1]
}, {
type: 'l',
values: [(x2 - x1), (y2 - y1)]
});
break;

case 'circle':
case 'ellipse':
if (type == 'circle') {
let r = atts.r;
let [cX, cY] = [atts.cx, atts.cy - atts.r];
pathData.push({
type: 'M',
values: [cX, cY]
}, {
type: 'a',
values: [r, r, 0, 0, 1, r, r]
}, {
type: 'a',
values: [r, r, 0, 0, 1, -r, r]
}, {
type: 'a',
values: [r, r, 0, 0, 1, -r, -r]
}, {
type: 'a',
values: [r, r, 0, 0, 1, r, -r]
}, {
type: 'z',
values: []
});

} else {
let rx = atts.rx;
let ry = atts.ry;
let [cX, cY] = [atts.cx, atts.cy - atts.ry];
pathData.push({
type: 'M',
values: [cX, cY]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, rx, ry]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, -rx, ry]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, -rx, -ry]
}, {
type: 'a',
values: [rx, ry, 0, 0, 1, rx, -ry]
}, {
type: 'z',
values: []
});
}
break;

case 'polygon':
case 'polyline':
let closePath = type == 'polygon' ? 'z' : '';
let points = atts.points.replace(/^\s+|\s+$|\s+(?=\s)/g, "").replaceAll(",", " ");
let pointArr = points.split(' ');
pathData.push({
type: 'M',
values: [+pointArr[0], +pointArr[1]]
});

for (let i = 2; i < pointArr.length; i += 2) {
let [x0, y0] = [+pointArr[i - 2], +pointArr[i - 1]];
let [x, y] = [+pointArr[i], +pointArr[i + 1]];
let com = {};

if (y == y0) {
com = {
type: 'h',
values: [x - x0]
}
} else if (x == x0) {
com = {
type: 'v',
values: [y - y0]
}
} else {
com = {
type: 'l',
values: [x - x0, y - y0]
}
}
pathData.push(com);
}
if (closePath) {
pathData.push({
type: 'z',
values: []
});
}
break;

//paths
default:
let dClean = atts.d.replace(/^\s+|\s+$|\s+(?=\s)/g, "").replaceAll(",", " ");
let dArr = dClean.replace(/([a-zA-Z])/g, " | $1").split(' | ');
dArr.shift();
for (let i = 0; i < dArr.length; i++) {
let command = dArr[i].trim().split(' ');
let type = command.shift();

command = command.map((x) => {
return parseFloat(x);
});
pathData.push({
type: type,
values: command
});
}
break;
}

// copy primitive's attributes to path
setAttributes(path, atts, exclude);
// round coordinates and replace primitive with path
path.setPathDataOpt(pathData, decimals);
primitive.replaceWith(path);
})
}
};


function setAttributes(el, attributes, exclude = []) {
for (key in attributes) {
if (exclude.indexOf(key) === -1) {
el.setAttribute(key, attributes[key]);
}
}
}

function getAttributes(el) {
let attArr = [...el.attributes];
let attObj = {};
attArr.forEach(function(att) {
attObj[att.nodeName] = att.nodeValue;
});
return attObj;
}


/**
* return rounded path data
* based on:
* https://github.com/jarek-foksa/path-data-polyfill/blob/master/path-data-polyfill.js
*/
if (!SVGPathElement.prototype.setPathDataOpt) {
SVGPathElement.prototype.setPathDataOpt = function(pathData, decimals = 3) {
let d = "";
if (pathData.length) {
for (let i = 0; i < pathData.length; i++) {
let seg = pathData[i];
let [type, values] = [seg.type, seg.values];
let valArr = [];
if (values.length) {
for (let v = 0; v < values.length; v++) {
val = parseFloat(values[v]);
valArr.push(+val.toFixed(decimals));
}
}
d += type;
if (valArr.length) {
d += valArr.join(" ").trim();
}
}
d = d.
replaceAll(' -', '-').
replaceAll(' 0.', ' .').
replaceAll(' z', 'z');
this.setAttribute("d", d);
}
};
}
<p><button type="button" onclick="getConvertedMarkup(svg, svgMarkup, 2)">Convert Primitives</button></p>
<svg id="svg" xmlns="http://www.w3.org/2000/svg" data-width="150px" data-height="30px" viewBox="0 0 150 30">
<polygon id="polygon" fill="#CCCCCC" stroke="#E3000F" points="7.9,22.8 3,14.3 7.9,5.8 17.6,5.8 22.5,14.3 17.6,22.8 " />
<polyline id="polyline" fill="none" stroke="#E3000F" points="40.9,22.8 31.1,22.8 26.2,14.3 31.1,5.8
40.9,5.8 45.8,14.3 " />
<rect id="rect" x="37.5%" y="20%" rx="2%" ry="5%" fill="none" stroke="#E3000F" width="6%" height="56%" />
<line id="line" fill="none" stroke="#E3000F" x1="50.5" y1="22.8" x2="52.5" y2="5.8" />
<circle id="circle" fill="none" stroke="#E3000F" cx="52%" cy="49%" r="8%" />
<ellipse id="ellipse" fill="none" stroke="#E3000F" cx="68%" cy="49%" rx="7%" ry="25%" />
<path id="piechart" transform="scale(0.9) translate(130, 6)" d="M 10 0 A 10 10 0 1 1 1.34 15 L 10 10 z"
fill="red" class="segment segment-1 segment-class" id="segment-01" />
</svg>
<h3>Output</h3>
<textarea name="svgMarkup" id="svgMarkup" style="width:100%; height:20em;"></textarea>

Codepen converter example

关于d3.js - SVG - 将所有形状/基元转换为 <path>,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/31575550/

48 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com