Reputation: 21410
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
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
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
Reputation: 21410
Sovled it:
beginPath
and fill
.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