【问题标题】:Invert paths on canvas在画布上反转路径
【发布时间】:2017-05-31 22:26:37
【问题描述】:

看看下面的svg。那里的路径几乎相同,但第二个路径是通过使用evenodd 填充并在其中的形状中添加一个完整的矩形来反转的。

body {
  background: linear-gradient(to bottom, blue, red);
}

svg {
  height: 12em;
  border: 1px solid white;
}

svg + svg {
  margin-left: 3em;
}
<svg viewBox="0 0 10 10">
  <path d="
    M 1 1 L 2 3 L 3 2 Z
    M 9 9 L 8 7 L 7 8 Z
  " />
</svg>

<svg viewBox="0 0 10 10">
  <path fill-rule="evenodd" d="
    M 0 0 h 10 v 10 h -10 z
    M 1 1 L 2 3 L 3 2 Z
    M 9 9 L 8 7 L 7 8 Z
  " />
</svg>

现在我想在canvas 上画同样的图片。第一张图没有问题:

~function () {
  var canvas = document.querySelector('canvas');
  var ctx = canvas.getContext('2d');
  
  var h = canvas.clientHeight, w = canvas.clientWidth;
  canvas.height = h;
  canvas.width = w;
  ctx.scale(h / 10, w / 10);
  
  ctx.beginPath();
  ctx.moveTo(1, 1);
  ctx.lineTo(2, 3);
  ctx.lineTo(3, 2);
  ctx.closePath();
  ctx.fill();

  ctx.beginPath();
  ctx.moveTo(9, 9);
  ctx.lineTo(8, 7);
  ctx.lineTo(7, 8);
  ctx.closePath();
  ctx.fill();
}()
body {
  background: linear-gradient(to bottom, blue, red);
}

canvas {
  height: 12em;
  border: 1px solid white;
}
&lt;canvas height="10" width="10"&gt;&lt;/canvas&gt;

但如果我需要画布具有透明背景,我该如何绘制第二个?
每个路径片段仅由行 L 组成,从 M 开始,以 Z 结束。
片段不重叠。

【问题讨论】:

    标签: javascript html canvas svg


    【解决方案1】:

    创建图像反转的最佳方法是使用 globalCompositeOperation = "destination-out" 覆盖原始图像

    填充规则的问题在于,很多时候用于创建形状的方法与其生成的图像的视觉表示不匹配。

    下一个 sn-p 显示了这种情况。只需穿过路径线即可快速渲染星星。 nonzero 填充规则创建我们想要的形状。但是如果我们试图通过定义围绕它的路径来反转它,它会失败,如果我们使用evenodd 规则,它也无法显示重叠区域。此外,添加外部框会增加笔触和填充,从而使图像和获得我们想要的东西所需的工作量进一步复杂化。

    const ctx = canvas.getContext("2d");
    const w = (canvas.width = innerWidth)*0.5;
    const h = (canvas.height = innerHeight)*0.5;
    // when there is a fresh context you dont need to call beginPath
    
    
    // when defining a new path (after beginPath or a fresh ctx) you 
    // dont need to use moveTo the path will start at the first point
    // you define
    for(var i = 0; i < 14; i ++){
      var ang = i * Math.PI * (10/14);
      var x = Math.cos(ang) * w * 0.7 + w; 
      var y = Math.sin(ang) * h * 0.7 + h; 
      ctx.lineTo(x,y);  
    }
    ctx.closePath();
    ctx.lineWidth = 5;
    ctx.lineJoin = "round";
    ctx.stroke();
    ctx.fillStyle = "red";
    ctx.fill();
    canvas.onclick = ()=>{
        ctx.rect(0,0,innerWidth,innerHeight);
        ctx.fillStyle = "blue";
        ctx.fill();
        info.textContent = "Result did not invert using nonzero fill rule";
        info1.textContent = "Click to see using evenodd fill";
        info1.className = info.className = "whiteText";
        canvas.onclick = ()=>{
           info.textContent = "Inverse image not the image wanted";
           info1.textContent = "Click to show strokes";
           info.className = info1.className = "blackText";
           ctx.fillStyle = "yellow";
           ctx.fill("evenodd");
            canvas.onclick = ()=>{
                info.textContent = "Strokes on boundary encroch on the image";
                 info1.textContent = "See next snippet using composite operations";
    
                ctx.stroke();
                ctx.lineWidth = 10;
                ctx.lineJoin = "round";
                ctx.strokeStyle = "Green";
                ctx.stroke();
    
            }
        
        }
    }
    body {
      font-family : "arial";
    }
    .whiteText { color : white }
    .blackText { color : black }
    canvas {
      position : absolute;
      top : 0px;
      left : 0px;
      z-index : -10;
    }
    <canvas id=canvas></canvas>
    <div id="info">The shape we want to invert</div>
    <div id="info1">Click to show result of attempting to invert</div>

    要绘制形状的反转,首先用不透明值(在本例中为黑色)填充所有像素。然后像往常一样定义形状。无需添加额外的路径点。

    在调用填充或描边之前,请将复合操作设置为“destination-out”,这意味着无论您在何处渲染像素,都会从目标中移除像素。然后像往常一样调用填充和描边函数。

    完成后,您恢复默认的复合操作

    ctx.globalCompositeOperation = "source-over";
    

    参见下一个示例。

        const ctx = canvas.getContext("2d");
        const w = (canvas.width = innerWidth)*0.5;
        const h = (canvas.height = innerHeight)*0.5;
        // first create the mask
        ctx.fillRect(10,10,innerWidth-20,innerHeight-20);
    
        // then create the path for the shape we want inverted
        for(var i = 0; i < 14; i ++){
          var ang = i * Math.PI * (10/14);
          var x = Math.cos(ang) * w * 0.7 + w; 
          var y = Math.sin(ang) * h * 0.7 + h; 
          ctx.lineTo(x,y);  
        }
        ctx.closePath();
        ctx.lineWidth = 5;
        ctx.lineJoin = "round";
        // now remove pixels where the shape is defined
        // both for the stoke and the fill
        ctx.globalCompositeOperation = "destination-out";
        ctx.stroke();
        ctx.fillStyle = "red";
        ctx.fill();
        canvas {
          position : absolute;
          top : 0px;
          left : 0px;
          z-index : -10;
          background: linear-gradient(to bottom, #6CF, #3A6, #4FA);
        }
     
        <canvas id=canvas></canvas>

    【讨论】:

    • 感谢您的精彩回答!
    【解决方案2】:

    ctx.fill(fillrule) 也接受 "evenodd" fillrule 参数,但在这种情况下甚至不需要它,因为您的三角形与您的矩形完全重叠。

    ~function () {
      var canvas = document.querySelector('canvas');
      var ctx = canvas.getContext('2d');
      
      var h = canvas.clientHeight, w = canvas.clientWidth;
      canvas.height = h;
      canvas.width = w;
      ctx.scale(h / 10, w / 10);
      
      ctx.beginPath(); // start our Path declaration
    
      ctx.moveTo(1, 1);
      ctx.lineTo(2, 3);
      ctx.lineTo(3, 2);
      // Actually closePath is generally only needed for stroke()
      ctx.closePath(); // lineTo(1,1)
    
      ctx.moveTo(9, 9);
      ctx.lineTo(8, 7);
      ctx.lineTo(7, 8);
      ctx.closePath(); // lineTo(9,9)
      ctx.rect(0,0,10,10) // the rectangle  
      
      ctx.fill();
      
    }()
    body {
      background: linear-gradient(to bottom, blue, red);
    }
    
    canvas {
      height: 12em;
      border: 1px solid white;
    }
    &lt;canvas height="10" width="10"&gt;&lt;/canvas&gt;

    如果你的三角形与路径的另一段重叠(这里是弧线)会很有用:

    var canvas = document.querySelectorAll('canvas');
      var h = canvas[0].clientHeight, w = canvas[0].clientWidth;
      drawShape(canvas[0].getContext('2d'), 'nonzero');
      drawShape(canvas[1].getContext('2d'), 'evenodd');
      
    function drawShape(ctx, fillrule) {
      ctx.canvas.height = h;
      ctx.canvas.width = w;
    
      ctx.scale(h / 10, w / 10);
      
      ctx.beginPath(); // start our Path declaration
    
      ctx.moveTo(1, 1);
      ctx.lineTo(2, 3);
      ctx.lineTo(3, 2);
      // here closePath is useful
      ctx.closePath(); // lineTo(1,1)
    
      ctx.arc(5,5,5,0,Math.PI*2)
    
      ctx.moveTo(9, 9);
      ctx.lineTo(8, 7);
      ctx.lineTo(7, 8);
      ctx.closePath(); // lineTo(9,9)
      ctx.rect(0,0,10,10) // the rectangle  
      
      ctx.fill(fillrule);
      ctx.fillStyle = 'white';
      ctx.setTransform(1,0,0,1,0,0);
      ctx.fillText(fillrule, 5, 12)
    }
    body {
      background: linear-gradient(to bottom, blue, red);
    }
    
    canvas {
      height: 12em;
      border: 1px solid white;
    }
    <canvas height="10" width="10"></canvas>
    <canvas height="10" width="10"></canvas>

    【讨论】:

    • 您能否澄清beginPathclosePath 的操作并删除第二个片段的关闭?
    • @Qwertiy,添加了一些 cmets,但基本上,beginPath 开始了一个新的路径声明。如果你不使用stroke() 方法,这个sn-p 实际上甚至不需要closePath。它只是当前完整段的lineTo 第一个点。段可以被moveToclosePath 打破,但fill() 无论如何都会包含这个lineTo
    【解决方案3】:

    解决了:

    • 仅使用一对beginPathfill
    • 手动将closePath替换为lineTo对应的点。

    它会给你一个倒置的图像:

    ~function () {
      var canvas = document.querySelector('canvas');
      var ctx = canvas.getContext('2d');
      
      var h = canvas.clientHeight, w = canvas.clientWidth;
      canvas.height = h;
      canvas.width = w;
      ctx.scale(h / 10, w / 10);
      
      ctx.beginPath(); // begin it once
    
      ctx.moveTo(0, 0); // Add full rectangle
      ctx.lineTo(10, 0);
      ctx.lineTo(10, 10);
      ctx.lineTo(0, 10);
    
      ctx.moveTo(1, 1);
      ctx.lineTo(2, 3);
      ctx.lineTo(3, 2);
      ctx.lineTo(1, 1); // not ctx.closePath();
    
      ctx.moveTo(9, 9);
      ctx.lineTo(8, 7);
      ctx.lineTo(7, 8);
      ctx.lineTo(9, 9);
    
      ctx.fill(); // And fill in the end
    }()
    body {
      background: linear-gradient(to bottom, blue, red);
    }
    
    canvas {
      height: 12em;
      border: 1px solid white;
    }
    &lt;canvas height="10" width="10"&gt;&lt;/canvas&gt;

    【讨论】:

      猜你喜欢
      • 2019-04-12
      • 1970-01-01
      • 2022-10-23
      • 2013-05-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-06-25
      • 1970-01-01
      相关资源
      最近更新 更多