Qwertiy
Qwertiy

Reputation: 21410

Invert paths on canvas

Take a look following svg. The paths there are almost the same, but the second one is inverted by using evenodd filling and adding a full rectangle to the shapes inside of it.

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>

Now I want to draw the same picture on the canvas. There are no problems with the first image:

~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;
}
<canvas height="10" width="10"></canvas>

But how can I draw the second if I need canvas to have transparent background?
Each path fragment consists only from lines L, start from M and end by Z.
Fragments don't overlap.

Upvotes: 1

Views: 2140

Answers (3)

Blindman67
Blindman67

Reputation: 54069

The best way to create an inverse of a image is draw over the original with the globalCompositeOperation = "destination-out"

The problem with the fill rules is that many times the method used to create a shape does not match the visual representation of the image it generates.

The next snippet shows such a case. The star is quickly rendered by just crossing path lines. The nonzero fill rule creates the shape we want. But if we attempt to invert it by defining a path around it, it fails, if we use the evenodd rule it also fails showing the overlapping areas. Additionally adding an outside box adds to the strokes as well as the fills further complicating the image and the amount of work that is needed to get what we want.

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>

To draw the inverse of a shape, first fill all the pixels with the opaque value (black in this case). Then define the shape as you would normally do. No need to add extra path points.

Before you call fill or stroke set the composite operation to "destination-out" which means remove pixels from the destination wherever you render pixels. Then just call the fill and stroke functions as normal.

Once done you restore the default composite operation with

ctx.globalCompositeOperation = "source-over";

See next example.

    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>

Upvotes: 4

Kaiido
Kaiido

Reputation: 136755

ctx.fill(fillrule) also accepts "evenodd" fillrule parameter, but in this case it is not even needed since your triangles entirely overlap with your rectangle.

~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;
}
<canvas height="10" width="10"></canvas>

It would have been useful if e.g you had your triangles overlapping with an other segment of the path (here an arc):

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>

Upvotes: 1

Qwertiy
Qwertiy

Reputation: 21410

Sovled it:

  • Use only one pair of beginPath and fill.
  • Replace closePath by manual lineTo to corresponding point.

And it would give you an inverted image:

~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;
}
<canvas height="10" width="10"></canvas>

Upvotes: 0

Related Questions