【问题标题】:Convert SVG Path d attribute to a array of points将 SVG Path d 属性转换为点数组
【发布时间】:2014-10-12 14:11:13
【问题描述】:

当我可以如下创建一行时:

var lineData = [{ "x": 50, "y": 50 }, {"x": 100,"y": 100}, {"x": 150,"y": 150}, {"x": 200, "y": 200}];
var lineFunction = d3.svg.line()
   .x(function(d) { return d.x; })
   .y(function(d) { return d.y; })
   .interpolate("basis");
var myLine = lineEnter.append("path")
   .attr("d", lineFunction(lineData))

现在我想在这个 lineArray 的第二个点添加一个文本:

lineEnter.append("text").text("Yaprak").attr("y", function(d){ 
console.log(d); // This is null
console.log("MyLine");
console.log(myLine.attr("d")) // This is the string given below, unfortunately as a String
// return lineData[1].x
return 10;

});

console.log(myLine.attr("d"))行的输出:

M50,50L58.33333333333332,58.33333333333332C66.66666666666666,66.66666666666666,83.33333333333331,83.33333333333331,99.99999999999999,99.99999999999999C116.66666666666666,116.66666666666666,133.33333333333331,133.33333333333331,150,150C166.66666666666666,166.66666666666666,183.33333333333331,183.33333333333331,191.66666666666663,191.66666666666663L200,200

我可以得到字符串格式的路径数据。我可以将此数据转换回 lineData 数组吗?或者,是否有其他简单的方法可以在附加文本时重新生成或获取 lineData?

请参考this JSFiddle。

【问题讨论】:

    标签: javascript svg d3.js


    【解决方案1】:

    我已经成功地使用它来渲染 x,y 点的列表:

    https://shinao.github.io/PathToPoints/

    代码超出了我可以放入此文本框的内容,但这里可能是一个好的开始: https://github.com/Shinao/PathToPoints/blob/master/js/pathtopoints.js#L209

    【讨论】:

      【解决方案2】:

      pathSegList 在旧版 Chrome 中受支持,自 Chrome 48 起已被删除。
      但是 Chrome 还没有实现new API。

      使用 path seg polyfill 来处理旧 API。

      使用path data polyfill 与new API 配合使用。推荐。

      var path = myLine.node();
      //Be sure you have added the pathdata polyfill to your page before use getPathData
      var pathdata = path.getPathData();
      console.log(pathdata);
      //you will get an Array object contains all path data details
      //like this:
      [
          {
              "type": "M",
              "values": [ 50, 50 ]
          },
          {
              "type": "L",
              "values": [ 58.33333333333332, 58.33333333333332 ]
          },
          {
              "type": "C",
              "values": [ 66.66666666666666, 66.66666666666666, 83.33333333333331, 83.33333333333331, 99.99999999999999, 99.99999999999999 ]
          },
          {
              "type": "C",
              "values": [ 116.66666666666666, 116.66666666666666, 133.33333333333331, 133.33333333333331, 150, 150 ]
          },
          {
              "type": "C",
              "values": [ 166.66666666666666, 166.66666666666666, 183.33333333333331, 183.33333333333331, 191.66666666666663, 191.66666666666663 ]
          },
          {
              "type": "L",
              "values": [ 200, 200 ]
          }
      ]
      

      【讨论】:

        【解决方案3】:

        我通过 Google 找到了这个问题。我需要的只是 SVG path 对象的 pathSegList 属性:

        var points = pathElement.pathSegList;
        

        每个点看起来像

        y: 57, x: 109, pathSegTypeAsLetter: "L", pathSegType: 4, PATHSEG_UNKNOWN: 0…}
        

        见

        【讨论】:

          【解决方案4】:

          有点笨拙,但您可以使用 animateMotion 沿路径为对象(例如矩形或圆形)设置动画,然后对对象的 x/y 位置进行采样。您将不得不做出一系列选择(例如,您以多快的速度为对象设置动画,您以多快的速度对 x/y 位置进行采样等)。您也可以多次运行此过程并取某种平均值或中位数。

          完整代码(查看实际操作:http://jsfiddle.net/mqmkc7xz/)

          <html>
            <body>
              <svg xmlns="http://www.w3.org/2000/svg" width="100" height="100">
                <path id="mypath"
                style="fill:none;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
                d="m 70,67 15,0 c 0,0 -7.659111,-14.20627 -10.920116,-27.28889 -3.261005,-13.08262 9.431756,-13.85172 6.297362,-15.57166 -3.134394,-1.71994 -7.526366,-1.75636 -2.404447,-3.77842 3.016991,-1.19107 9.623655,-5.44678 0.801482,-9.67404 C 76.821958,10 70,10 70,10"
                />
              </svg>
              <div id="points"></div>
              <script>
              /**
               * Converts a path into an array of points.
               *
               * Uses animateMotion and setInterval to "steal" the points from the path.
               * It's very hacky and I have no idea how well it works.
               *
               * @param SVGPathElement  path to convert
               * @param int             approximate number of points to read
               * @param callback        gets called once the data is ready
               */
              function PathToPoints(path, resolution, onDone) {
                var ctx = {};
                ctx.resolution = resolution;
                ctx.onDone = onDone;
                ctx.points = [];
                ctx.interval = null;
          
                // Walk up nodes until we find the root svg node
                var svg = path;
                while (!(svg instanceof SVGSVGElement)) {
                  svg = svg.parentElement;
                }
                // Create a rect, which will be used to trace the path
          
                var rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
                ctx.rect = rect;
                svg.appendChild(rect);
          
                var motion = document.createElementNS("http://www.w3.org/2000/svg", "animateMotion");
                motion.setAttribute("path", path.getAttribute("d"));
                motion.setAttribute("begin", "0");
                motion.setAttribute("dur", "3"); // TODO: set this to some larger value, e.g. 10 seconds?
                motion.setAttribute("repeatCount", "1");
                motion.onbegin = PathToPoints.beginRecording.bind(this, ctx);
                motion.onend = PathToPoints.stopRecording.bind(this, ctx);
          
                // Add rect
                rect.appendChild(motion);
              }
          
              PathToPoints.beginRecording = function(ctx) {
                var m = ctx.rect.getScreenCTM();
                ctx.points.push({x: m.e, y: m.f});
                ctx.interval = setInterval(PathToPoints.recordPosition.bind(this, ctx), 1000*3/ctx.resolution);
              }
          
              PathToPoints.stopRecording = function(ctx) {
                clearInterval(ctx.interval);
          
                // Remove the rect
                ctx.rect.remove();
          
                ctx.onDone(ctx.points);
              }
          
              PathToPoints.recordPosition = function(ctx) {
                var m = ctx.rect.getScreenCTM();
                ctx.points.push({x: m.e, y: m.f});
              }
              PathToPoints(mypath, 100, function(p){points.textContent = JSON.stringify(p)});
              </script>
            </body>
          </html>
          

          【讨论】:

            【解决方案5】:

            SVGPathElement API 具有用于获取此信息的内置方法。您不需要自己解析数据字符串。

            由于您将行的选择存储为变量,因此您可以使用 myLine.node() 轻松访问路径元素的 api,以引用 path 元素本身。

            例如:

            var pathElement = myLine.node();
            

            然后您可以通过访问pathSegList 属性来访问用于构造路径的命令列表:

            var pathSegList = pathElement.pathSegList;
            

            使用该对象的length 属性,您可以轻松地遍历它以获取与每个路径段关联的坐标:

            for (var i = 0; i < pathSegList.length; i++) {
              console.log(pathSegList[i]);
            }
            

            检查控制台输出,您会发现每个路径段都有x 和y 的属性,代表该段的端点。对于贝塞尔曲线、圆弧等,控制点也根据需要以x1、y1、x2和y2给出。

            在您的情况下,无论您是使用此方法还是选择自己解析字符串,您都会遇到困难,因为您使用interpolate('basis') 进行行插值。因此,线生成器输出 6 个命令(在您的特定情况下)而不是 4 个,并且它们的端点并不总是对应于数据中的原始点。如果您使用interpolate('linear'),您将能够重建原始数据集,因为线性插值与路径数据输出一一对应。

            假设您使用线性插值,重建原始数据集可以如下完成:

            var pathSegList = myLine.node().pathSegList;
            
            var restoredDataset = [];
            
            // loop through segments, adding each endpoint to the restored dataset
            for (var i = 0; i < pathSegList.length; i++) {
              restoredDataset.push({
                "x": pathSegList[i].x,
                "y": pathSegList[i].y
              })
            }
            

            编辑:

            至于在附加文本时使用原始数据...我假设您正在寻找将标签附加到点,没有必要经历重建数据的所有麻烦。事实上,真正的问题是你从来没有使用数据绑定来制作你的折线图。尝试使用.datum() 方法为您的路径绑定数据,并使用.data() 方法为标签绑定数据。此外,您可能想要重命名lineEnter,因为您没有使用输入选择,它只是代表一个组。例如:

            // THIS USED TO BE CALLED `lineEnter`
            var lineGroup = svgContainer.append("g");
            
            var myLine = lineGroup.append("path")
                // HERE IS WHERE YOU BIND THE DATA FOR THE PATH
                .datum(lineData)
                // NOW YOU SIMPLY CALL `lineFunction` AND THE BOUND DATA IS USED AUTOMATICALLY
                .attr("d", lineFunction)
                .attr("stroke", "blue")
                .attr("stroke-width", 2)
                .attr("fill", "none");
            
            // FOR THE LABELS, CREATE AN EMPTY SELECTION
            var myLabels = lineGroup.selectAll('.label')
                // FILTER THE LINE DATA SINCE YOU ONLY WANT THE SECOND POINT
                .data(lineData.filter(function(d,i) {return i === 1;})
                // APPEND A TEXT ELEMENT FOR EACH ELEMENT IN THE ENTER SELECTION
                .enter().append('text')
                // NOW YOU CAN USE THE DATA TO SET THE POSITION OF THE TEXT
                .attr('x', function(d) {return d.x;})
                .attr('y', function(d) {return d.y;})
                // FINALLY, ADD THE TEXT ITSELF
                .text('Yaprak')
            

            【讨论】:

            • 这仍然适用于大多数浏览器吗?我很确定我在 Chrome 48 中看到 undefined 代表 .pathSegList
            • @qwwqwwq pathSegList 已弃用。请参阅此答案以获取替代方案:stackoverflow.com/a/34359059/1525769
            • @qwwqwwq 他们弃用了它(破坏了所有使用它的应用程序),因为 提出 新的 API 作为新 SVG 标准的替代品。当然,几个月过去了,Chrome 仍然没有任何优化的 API 来获取路径段,不是旧的,也不是新的。火焰派对请看这里:bugs.chromium.org/p/chromium/issues/detail?id=539385
            • 请原谅我的天真,但getPathData 将返回一个正确的点数组,MCVHZ 的东西是正确的。但是,如果 svg 路径被缩小,例如 h 值没有 y,您如何找到 x/y 值,您是否必须遍历数组并找到最接近的前面 y 值?
            【解决方案6】:

            您可以通过拆分 L、M 和 C 字符上的字符串来将行拆分为单独的命令:

            var str = "M50,50L58.33333333333332,58.33333333333332C66.66666666666666,
              66.66666666666666,83.33333333333331,83.33333333333331,
              99.99999999999999,99.99999999999999C116.66666666666666,116.66666666666666,
              133.33333333333331,133.33333333333331,150,150C166.66666666666666,
              166.66666666666666,183.33333333333331,183.33333333333331,191.66666666666663,
              191.66666666666663L200,200"
            
            var commands = str.split(/(?=[LMC])/);
            

            这给出了用于渲染路径的命令序列。每个都是由一个字符(L、M 或 C)组成的字符串,后跟一串用逗号分隔的数字。它们看起来像这样:

            "C66.66666666666666,66.66666666666666,83.33333333333331,
            83.33333333333331,99.99999999999999,99.99999999999999"
            

            这描述了通过三个点 [66,66]、[83,83] 和 [99,99] 的曲线。您可以使用另一个 split 命令和一个循环将它们处理成对点数组,包含在地图中:

            var pointArrays = commands.map(function(d){
                var pointsArray = d.slice(1, d.length).split(',');
                var pairsArray = [];
                for(var i = 0; i < pointsArray.length; i += 2){
                    pairsArray.push([+pointsArray[i], +pointsArray[i+1]]);
                }
                return pairsArray;
            });
            

            这将返回一个数组,其中包含每个命令作为长度为 2 的数组的数组,每个数组是路径对应部分中一个点的 (x,y) 坐标对。

            您还可以修改map 中的函数以返回包含命令类型和数组中的点的对象。

            编辑: 如果您希望能够访问lineData,可以将其作为数据添加到组中,然后将路径附加到组中,并将文本附加到组中。

            var group = d3.selectAll('g').data([lineData])
              .append('g');
            
            var myLine = group.append('path')
              .attr('d', function(d){ return lineFunction(d); });
            
            var myText = group.append('text')
              .attr('text', function(d){ return 'x = ' + d[1][0]; });
            

            与逆向工程路径相比,这将是一种更 d3 式的数据访问方式。也可能更容易理解。

            More info on SVG path elements

            【讨论】:

            • 这会有问题,因为“C”命令后面的一些点是指控制点,而不是线实际通过的点。使用路径元素的pathSegList 属性,您可以确定路径数据中的哪些点实际上代表输出线上的点。请参阅我发布的答案以进行澄清。
            • 编辑:我添加了一种获取数据的方法,而无需担心解析路径数据。
            • 请注意,此解决方案不考虑 h 或 v 命令,它们将仅提供单个 x 或 y 参数,而不是一对。
            猜你喜欢
            • 2020-12-27
            • 2017-01-17
            • 1970-01-01
            • 1970-01-01
            • 2017-12-15
            • 2017-07-02
            • 2015-10-13
            相关资源
            最近更新 更多