【问题标题】:dotted stroke in <canvas><canvas> 中的虚线笔划
【发布时间】:2011-06-02 09:08:20
【问题描述】:

我猜想设置像 CSS 这样的笔画属性是不可能的,这很容易。使用 CSS,我们有虚线、点线、实线,但在画布上绘制线条/或笔触时,这似乎不是一个选项。您是如何实现的?

我见过一些例子,但是对于这样一个愚蠢的功能,它们真的很长。

例如:

http://groups.google.com/group/javascript-information-visualization-toolkit/browse_thread/thread/22000c0d0a1c54f9?pli=1

【问题讨论】:

    标签: javascript html canvas


    【解决方案1】:

    HTML5 Canvas 规范目前不支持虚线。

    看看这个:

    http://davidowens.wordpress.com/2010/09/07/html-5-canvas-and-dashed-lines/

    查看 Raphael JS 库:

    http://raphaeljs.com/

    【讨论】:

      【解决方案2】:

      有趣的问题!我编写了虚线的自定义实现;你可以try it out here。我采用了 Adob​​e Illustrator 的路线,并允许您指定一个破折号/间隙长度数组。

      对于 stackoverflow 的后代,这是我的实现(针对 s/o 线宽略有改变):

      var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
      if (CP && CP.lineTo){
        CP.dashedLine = function(x,y,x2,y2,dashArray){
          if (!dashArray) dashArray=[10,5];
          if (dashLength==0) dashLength = 0.001; // Hack for Safari
          var dashCount = dashArray.length;
          this.moveTo(x, y);
          var dx = (x2-x), dy = (y2-y);
          var slope = dx ? dy/dx : 1e15;
          var distRemaining = Math.sqrt( dx*dx + dy*dy );
          var dashIndex=0, draw=true;
          while (distRemaining>=0.1){
            var dashLength = dashArray[dashIndex++%dashCount];
            if (dashLength > distRemaining) dashLength = distRemaining;
            var xStep = Math.sqrt( dashLength*dashLength / (1 + slope*slope) );
            if (dx<0) xStep = -xStep;
            x += xStep
            y += slope*xStep;
            this[draw ? 'lineTo' : 'moveTo'](x,y);
            distRemaining -= dashLength;
            draw = !draw;
          }
        }
      }
      

      要绘制一条从20,150170,10 的线,其中包含 30 像素长的虚线和 10 像素的间隙,您可以使用:

      myContext.dashedLine(20,150,170,10,[30,10]);
      

      要绘制交替的短划线和点,请使用(例如):

      myContext.lineCap   = 'round';
      myContext.lineWidth = 4; // Lines 4px wide, dots of diameter 4
      myContext.dashedLine(20,150,170,10,[30,10,0,10]);
      

      0 的“非常短”虚线长度与圆角 lineCap 相结合会在您的线上形成点。

      如果有人知道访问画布上下文路径当前点的方法,我很想知道,因为它可以让我将其写为ctx.dashTo(x,y,dashes),而不是要求您重新- 指定方法调用的起点。

      【讨论】:

      • @Sam 你不是唯一一个认为我们不应该做这么多工作来在 Canvas 上绘制虚线的人:lists.whatwg.org/pipermail/whatwg-whatwg.org/2007-May/…
      • 这似乎是我找到的最好的实现。但是,我很好奇第一行完成了什么。我希望 && 运算符返回一个布尔结果,然后将其分配给 CP。我是 JavaScript 的新手,所以我想知道我是否遗漏了一些基本的东西。
      • @Jon JavaScript 的 “布尔运算符” 实际上是 “保护运算符”。使用短路评估,它们返回最后评估的值的“真实性”。以下是一些真实的陈述:42 == 42 || "hi"; "hi" == false || "hi"; true == 17 &amp;&amp; true; false == 17 &amp;&amp; false; "" == "" &amp;&amp; false。打开您友好的开发人员控制台并自己进行试验。 :) 如果CanvasRenderingContext2D 存在,第一行将CP 设置为原型,否则将其设置为undefined
      • 当x坐标差为0时,你除以零,不起作用。
      • @HonzaBrabec 是的,你是对的。 1e15 hack 解决了这个问题。还有其他更简洁的方法可以做到这一点,但这需要最少的代码更改并且足够有效。
      【解决方案3】:

      这个 Phrogz 代码的简化版本利用了 Canvas 的内置转换功能,还可以处理特殊情况,例如当 dx = 0

      var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
      if (CP.lineTo) {
          CP.dashedLine = function(x, y, x2, y2, da) {
              if (!da) da = [10,5];
              this.save();
              var dx = (x2-x), dy = (y2-y);
              var len = Math.sqrt(dx*dx + dy*dy);
              var rot = Math.atan2(dy, dx);
              this.translate(x, y);
              this.moveTo(0, 0);
              this.rotate(rot);       
              var dc = da.length;
              var di = 0, draw = true;
              x = 0;
              while (len > x) {
                  x += da[di++ % dc];
                  if (x > len) x = len;
                  draw ? this.lineTo(x, 0): this.moveTo(x, 0);
                  draw = !draw;
              }       
              this.restore();
          }
      }
      

      我认为我的计算是正确的,它似乎可以渲染。

      【讨论】:

      • 只想提一下 Phrogz 的解决方案不适用于垂直线。此解决方案有效。
      • 防止已经定义的虚线函数可能会很好:CP.dashedLine = CP.dashedLine || function...
      • 另外,CP 可以是未定义的(如果是!window.CanvasRenderingContext2D),在这种情况下CP.lineTo 位会失败。
      【解决方案4】:

      Mozilla 一直在为画布开发 implementation of dashed stroking,因此我们可能会在不久的将来看到它添加到规范中。

      【讨论】:

      • 几个月前已验证修复。
      【解决方案5】:

      我在 Mozilla 规范中找到了属性 mozDashmozDashOffset
      http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/canvas/nsIDOMCanvasRenderingContext2D.idl

      它们可能用于控制破折号,但我没有使用它们。

      【讨论】:

        【解决方案6】:

        Phroz 的解决方案很棒。但是当我在我的应用程序中使用它时,我发现了两个错误。

        以下代码是 Phroz 的调试(并重构以提高可读性)版本。

        // Fixed: Minus xStep bug (when x2 < x, original code bugs)
        // Fixed: Vertical line bug (when abs(x - x2) is zero, original code bugs because of NaN)
        var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
        if(CP && CP.lineTo) CP.dashedLine = function(x, y, x2, y2, dashArray){
            if(! dashArray) dashArray=[10,5];
            var dashCount = dashArray.length;
            var dx = (x2 - x);
            var dy = (y2 - y);
            var xSlope = (Math.abs(dx) > Math.abs(dy));
            var slope = (xSlope) ? dy / dx : dx / dy;
        
            this.moveTo(x, y);
            var distRemaining = Math.sqrt(dx * dx + dy * dy);
            var dashIndex = 0;
            while(distRemaining >= 0.1){
                var dashLength = Math.min(distRemaining, dashArray[dashIndex % dashCount]);
                var step = Math.sqrt(dashLength * dashLength / (1 + slope * slope));
                if(xSlope){
                    if(dx < 0) step = -step;
                    x += step
                    y += slope * step;
                }else{
                    if(dy < 0) step = -step;
                    x += slope * step;
                    y += step;
                }
                this[(dashIndex % 2 == 0) ? 'lineTo' : 'moveTo'](x, y);
                distRemaining -= dashLength;
                dashIndex++;
            }
        }
        

        【讨论】:

          【解决方案7】:

          有一种更简单的方法可以做到这一点。根据http://www.w3.org/TR/2dcontext/#dom-context-2d-strokestylestrokeStyle 接受字符串、CanvasGradients 或 CanvasPatterns。所以我们只拍一张这样的照片:

            <img src="images/dashedLineProto.jpg" id="cvpattern1" width="32" height="32" />
          

          将其加载到画布中,并用它绘制我们的小矩形。

            var img=document.getElementById("cvpattern1");
            var pat=ctx.createPattern(img,"repeat");
            ctx.strokeStyle = pat;
            ctx.strokeRect(20,20,150,100);
          

          这不会产生完美的虚线,但它非常简单且可修改。当您绘制非水平或垂直的线条时,结果当然可能会变得不完美,虚线图案可能会有所帮助。

          PS。请记住,当您尝试在代码中使用来自外部来源的 imgs 时,SOP 适用。

          【讨论】:

            【解决方案8】:

            Firefox at least有支持

            ctx.mozDash = [5,10];
            

            似乎 ctx.webkitLineDash 以前工作过,但他们删除了它,因为它有一些 compabillity issues

            W3C specs 表示 ctx.setLineDash([5,10]);,但它似乎还没有在任何地方实现。

            【讨论】:

              【解决方案9】:

              目前至少 setLineDash([5,10]) 适用于 Chrome,而 ctx.mozDash = [5,10] 适用于 FF:

              var c=document.getElementById("myCanvas");
              var ctx=c.getContext("2d");
              
              if ( ctx.setLineDash !== undefined )   ctx.setLineDash([5,10]);
              if ( ctx.mozDash !== undefined )       ctx.mozDash = [5,10];
              
              ctx.beginPath();              
              ctx.lineWidth="2";
              ctx.strokeStyle="green";
              ctx.moveTo(0,75);
              ctx.lineTo(250,75);
              ctx.stroke();
              

              设置为 null 使线条成为实线。

              【讨论】:

              【解决方案10】:

              我修改了dashedLine 函数以添加对偏移的支持。如果浏览器支持ctx.setLineDashctx.lineDashOffset,它将使用本机虚线。

              示例:http://jsfiddle.net/mLY8Q/6/

              var CP = window.CanvasRenderingContext2D && CanvasRenderingContext2D.prototype;
              if (CP.lineTo) {
              
                  CP.dashedLine = CP.dashedLine || function (x, y, x2, y2, da, offset) {
              
                      if (!da) da = [10, 5];
                      if (!offset) offset = 0;
              
                      if (CP.setLineDash && typeof (CP.lineDashOffset) == "number") {
                          this.save();
                          this.setLineDash(da);
                          this.lineDashOffset = offset;
              
                          this.moveTo(x, y);
                          this.lineTo(x2, y2);
              
                          this.restore();
                          return;
                      }
              
              
                      this.save();
                      var dx = (x2 - x),
                          dy = (y2 - y);
                      var len = Math.sqrt(dx * dx + dy * dy);
                      var rot = Math.atan2(dy, dx);
                      this.translate(x, y);
                      this.moveTo(0, 0);
                      this.rotate(rot);
                      var dc = da.length;
                      var di = 0;
              
                      var patternLength = 0;
                      for (var i = 0; i < dc; i++) {
                          patternLength += da[i];
                      }
                      if (dc % 2 == 1) {
                          patternLength *= 2;
                      }
              
                      offset = offset % patternLength;
                      if (offset < 0) {
                          offset += patternLength;
                      }
              
                      var startPos = 0;
                      var startSegment = 0;
                      while (offset >= startPos) {
              
              
              
                          if (offset >= startPos + da[startSegment % dc]) {
                              startPos += da[startSegment % dc];
                              startSegment++;
                          } else {
                              offset = Math.abs(offset - startPos);
                              break;
                          }
              
              
                          if (startSegment > 100) break;
                      }
                      draw = startSegment % 2 === 0;
                      x = 0;
                      di = startSegment;
              
              
                      while (len > x) {
                          var interval = da[di++ % dc];
                          if (x < offset) {
                              interval = Math.max(interval - offset, 1);
                              offset = 0;
                          }
              
                          x += interval;
                          if (x > len) x = len;
                          draw ? this.lineTo(x, 0) : this.moveTo(x, 0);
                          draw = !draw;
                      }
                      this.restore();
                  };
              }
              

              【讨论】:

                【解决方案11】:

                看起来 context.setLineDash 已经实现了很多。 见this

                " context.setLineDash([5]) 将产生一条虚线,其中虚线和空格的大小均为 5 像素。 "

                【讨论】: