【问题标题】:Is it possible to make SVG appear consistent around sharp corners with stroke-dasharray?是否可以使用 stroke-dasharray 使 SVG 在尖角处看起来一致?
【发布时间】:2017-05-20 08:30:54
【问题描述】:

我正在开发一个交互式应用程序,允许通过 SVG 直接操作形状。我将从一个具体的例子开始,然后问一个一般性的问题。给出的示例是它在 Chrome 中的呈现方式。

鉴于stroke-dasharray 的许多可能值,this star's 手臂的笔划不一致。 3 个边缘看起来很钝,2 个边缘看起来很锋利。 stroke-linejoin 会改变星星的外观,但不能解决每条手臂的不一致问题。

<svg width="800" height="600" xmlns="http://www.w3.org/2000/svg"> 
  <polygon stroke="red"
           stroke-width="20"
           stroke-dasharray="5 5"
           points="350,75  379,161 469,161 397,215
                      423,301 350,250 277,301 303,215
                      231,161 321,161" />
</svg>

虽然使用stroke-dashoffset 摆弄每个手臂的笔画可能看起来一致在这种特殊情况下,但我怀疑一个人能否使笔画的急转弯看起来一致一般 stroke-dasharray 有效。但是,我的团队内部要求使它们保持一致,因此我必须对此进行调查。

所以要确认一下:是否有一个通用的解决方案可以使用stroke-dasharray 使尖角周围的笔触保持一致?

【问题讨论】:

  • 确保您的星边长度一致。然后确保您的破折号图案制作准确,以便每个破折号的位置对于每个边长都是一致的。
  • @PaulLeBeau 这种推理能否普遍适用于用户输入的折线?如果我可以为可变长度的边建立这些条件,我会感到惊讶。
  • 并非如此。不适用于线段可以是任意长度的折线。您想要一个在折线长度上大小不同的虚线图案吗?
  • @PaulLeBeau 这不是我的偏好,但我必须在这方面升级,看看他们是否想要。这可能在单个 元素中吗?
  • 技术上是的。但是你最终会得到很长的破折号数组序列。

标签: css dom svg


【解决方案1】:

对于具有不同线段长度的一般折线的情况,答案是“不容易”。

从技术上讲这是可能的,但您必须创建一个虚线图案,它是折线的总长度,并单独列出每个虚线段。图案的每个部分都经过精心设计,以适合每个线段的长度。这将导致非常长的破折号模式。显然,它需要为每条折线单独生成。

<svg width="400" height="400">
  
  <polygon points="50,100, 200,300, 350,300, 350,100"
           stroke="red" stroke-width="30"
           stroke-dasharray="50 50 50 50
                             100
                             50
                             90
                             40 40 40
                             100
                             60 60 60 60 0"/>
  
</svg>

【讨论】:

    【解决方案2】:

    一种可能的解决方法是使用标记来掩盖绘图的不一致:

    <svg width="800" height="600" xmlns="http://www.w3.org/2000/svg"> 
      <defs>
         <marker id="red-circle"
          viewBox="0 0 10 10" refX="5" refY="5" orient="auto" >
             <circle fill="red" cx="5" cy="5" r="5"/>
          </marker>
        
      </defs>
      <polygon filter="url(#trythis)" stroke="red"
               stroke-width="20"
               stroke-dasharray="5 5"
               points="350,75  379,161 469,161 397,215
                          423,301 350,250 277,301 303,215
                          231,161 321,161" 
                          marker-mid="url(#red-circle)"                                           
                          marker-end="url(#red-circle)"/>
    </svg>

    【讨论】:

      【解决方案3】:

      更新:此策略现在可用于 svg &lt;path&gt; 元素(包括曲线段),而不仅仅是 &lt;polygon&gt; 元素。我相信,只要稍加修改,它就可以用于任何 SVG 形状,尽管我在这里只对多边形和路径进行了演示。

      多边形(不需要任何 polyfill)

      下面的函数允许您以编程方式计算所需的破折号和间隙长度,并将它们作为stroke-dasharray 应用于polygon 元素。作为奖励,它还允许您选择是否要在所有角落(左图)或没有角落(右图)使用破折号。

      对于每个多边形线段,该函数计算线段的长度,然后计算以半长间隙开始和结束该线段所需的破折号数。然后它会计算所需的确切破折号长度并创建必要的破折号/间隙stroke-dasharray。例如,如果有空间容纳 3 个长度为 d 的破折号(相当于 6 个破折号或间隙),则 stroke-dasharray 将是 d/2 d d d d d d/2。这将用半破折号开始和结束线段,如下所示:

      xx----xxxx----xxxx----xx

      对每条边执行此操作,将一条边的最后半长破折号与下一条边的前半长破折号结合起来,例如...

      xx----xxxx----xxxx----xx + XXX------XXXXXX------XXXXXX------XXX

      ...变成...

      xx----xxxx----xxxx----xxXXX------XXXXXX------XXXXXX------XXX

      该函数还允许您将noneFlag 设置为true(默认为false),将笔划从在所有角落都有破折号转换为在没有角落有破折号。它通过简单地在stroke-dasharray 的开头添加一个零来实现这一点,有效地将所有破折号转换为空白,并将所有空白转换为破折号。每个生成的线段将如下所示:

      --xxxx----xxxx----xxxx--

      注意线段开始和结束处的半间隙(而不是半破折号)。

      dashesAtCorners(document.querySelector('#one'), 5      );
      dashesAtCorners(document.querySelector('#two'), 5, true);
      
      function dashesAtCorners(polygon, aveDashSize, noneFlag) {
        const coordinates = c = polygon.getAttribute('points').replace(/,| +/g, ' ')
          .trim().split(' ').map(n => +n); // extract points' coordinates from polygon
        c.push(c[0], c[1]); // repeat the 1st point's coordinates at the end
        const dashes = d = noneFlag ? [0,0] : [0]; // if noneFlag, prepend extra zero
        for (s = 0; s < c.length - 2; s += 2) { // s is line segment number * 2
          const dx = c[s]-c[s+2], dy = c[s+1]-c[s+3], segLen = Math.sqrt(dx*dx+dy*dy),
            numDashes = n = Math.floor(0.5 + segLen / aveDashSize / 2),
            dashLen = len = segLen / n / 2; // calculate # of dashes & dash length
          d.push((d.pop() + len) / 2); // join prev seg's last dash, this seg's 1st dash
          (i => {while (i--) {d.push(len,len)}})(n); // fill out line with gaps & dashes
        }
        polygon.setAttribute('stroke-dasharray', d.join(' '));
      }
      <svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
        <g stroke="red" stroke-width="20" transform="scale(0.7)">
          <polygon id="one" transform="translate(-200, -40)"
                   points="350,75  379,161 469,161 397,215
                              423,301 350,250 277,301 303,215
                              231,161 321,161"
                              />
          <polygon id="two" transform="translate(100, -40)"
                   points="350,75  379,161 469,161 397,215
                              423,301 350,250 277,301 303,215
                              231,161 321,161"
                              />
        </g>
      </svg>

      上述策略通常可以应用于任何多边形元素,对称或不对称。例如,尝试任意更改示例星的任何坐标,它仍然可以工作。 (但是,请注意,使角太“尖”,即角度非常小,会改变其笔触外观,从而使角从“尖锐”变为“钝”。我不知道一般规则为此,例如角度阈值/截止。我也不知道在实现此限制时是否存在浏览器差异。您知道。除此之外,该策略通常适用于任何多边形。)

      路径(需要 polyfill,截至 2017 年 2 月中旬)

      但是,上述策略不能完全按照写入路径元素的方式应用。一个很大的区别是多边形只有直边,而路径也可以有曲线。将此策略应用于路径元素需要进行修改,以计算路径段的长度,如上述策略计算直多边形边的长度。对于路径,您需要检索单个(直线或弯曲)路径段,然后使用 getTotalLength 方法确定段长度。然后,您将以与上述代码使用每个直多边形边的长度相同的方式进行上述计算。唉,我们目前处于 2 个可用于此目的的 SVG API 之间的无人区:一个已弃用的旧的 (pathSegList) 和一个尚未可用的替代 (getPathData)。幸运的是,有polyfills for both the older and newer APIs 可以使用。请注意,getPathData polyfill 不能直接用于&lt;use&gt; 元素(尽管我想它可以用于&lt;use&gt; 元素使用的&lt;defs&gt; 部分中的形状元素,尽管我没有具体说明检查了这个)。

      下图显示了来自this jsFiddle 使用the polyfill for getPathData, etc. 的屏幕截图。

      撇开 polyfill,jsFiddle 的代码如下:

      html:

      <span>Set average dash length:</span><input type="range" min="4" max="60" value="60"/>
      <span id="len">60</span>
      <svg width="800" height="600" xmlns="http://www.w3.org/2000/svg">
        <g stroke="red" stroke-width="20" transform="scale(0.7)">
          <path id="one" transform="translate(-200, -40)"
            d="M350,75 C
            360,140 420,161 469,161
            400,200 410,260 423,301
            380,270 330,270 277,301
            280,250 280,200 231,161
            280,160 320,140 350,75
            Z"
            />
          <path id="two" transform="translate(200, -40)"
            d="M350,75 C
            360,140 420,161 469,161
            400,200 410,260 423,301
            380,270 330,270 277,301
            280,250 280,200 231,161
            280,160 320,140 350,75
            Z"
            />
          <path id="three" transform="translate(600, -40)"
            d="M350,75 C
            360,140 420,161 469,161
            400,200 410,260 423,301
            380,270 330,270 277,301
            280,250 280,200 231,161
            280,160 320,140 350,75
            Z"
            />
        </g>
        <text transform="translate(55, 230)">Normal dashes</text>
        <text transform="translate(330, 230)">Dashes at corners</text>
        <text transform="translate(595, 230)">No dashes at corners</text>
      </svg>
      

      js:

      setDashes(60);
      
      document.querySelector('input').oninput = evt => {
          const dashLen = evt.target.value;
        document.querySelector('#len').innerHTML = dashLen;
        setDashes(dashLen);
      };
      
      function setDashes(dashLen) {
        document.querySelector('#one').setAttribute('stroke-dasharray', dashLen);
        dashesAtCorners(document.querySelector('#two'  ), dashLen      );
        dashesAtCorners(document.querySelector('#three'), dashLen, true);
      }
      
      function getSegLen(pathData, idx) {
        const svgNS = "http://www.w3.org/2000/svg";
        const currSeg = pathData[idx];
        const prevSeg = pathData[idx - 1];
        const prevSegVals = prevSeg.values;
        const startCoords =
          'M' +
          prevSegVals[prevSegVals.length - 2] +
          ',' +
          prevSegVals[prevSegVals.length - 1];
        const segData = currSeg.type + currSeg.values;
        const segD = startCoords + segData;
        const newElmt = document.createElementNS(svgNS, "path");
        newElmt.setAttributeNS(null, "d", segD);
        return newElmt.getTotalLength();
      }
      
      function dashesAtCorners(element, aveDashSize, noneFlag) {
        const pathData = element.getPathData();
        const dashes = d = noneFlag ? [0,0] : [0]; // if noneFlag, prepend extra zero
        const internalSegments = pathData.slice(1, -1);
          for (segNum = 1; segNum < pathData.length - 1; segNum += 1) {
          const segLen = getSegLen(pathData, segNum);
          const numDashes = Math.floor(0.5 + segLen / aveDashSize / 2);
          const dashLen = segLen / numDashes / 2;
          // calculate # of dashes & dash length
          dashes.push((dashes.pop() + dashLen) / 2); // join prev seg's last dash, this seg's 1st dash
          (dashNum => {while (dashNum--) {dashes.push(dashLen,dashLen)}})(numDashes); // fill out line with gaps & dashes
        }
        element.setAttribute('stroke-dasharray', dashes.join(' '));
      }
      

      【讨论】:

      • 感谢您提供有趣的解决方案!这是可以应用于任意&lt;path&gt; 元素的通用解决方案,还是仅适用于对称多边形?
      • @SageGerard,请参阅我在回答中添加的最后两段以解决您的评论。快速回答:正如所写,该解决方案适用于任何任意多边形(对称或非对称),但不适用于路径,尽管从概念上讲它可能是。
      • @SageGerard,我现在更新了我的答案以使用 getPathData polyfill 将此策略应用于 &lt;path&gt; 元素,包括具有弯曲路径段的元素。
      • 出色的工作,一个非常被低估的答案。感谢您的奉献精神和解决难题的意愿!
      猜你喜欢
      • 2013-12-23
      • 2016-03-13
      • 2019-04-03
      • 2021-08-08
      • 2020-12-28
      • 2018-12-27
      • 2018-05-12
      • 2019-08-18
      • 2014-04-02
      相关资源
      最近更新 更多