How can I create a perspective effect within a canvas? - javascript

I know how to create a perspective effect in vanilla CSS, but how can I create this effect in a canvas?
.scene {
width: 200px;
height: 200px;
border: 2px solid black;
margin: 40px;
}
.panel {
width: 100%;
height: 100%;
background: red;
/* perspective function in transform property */
transform: perspective(600px) rotateY(45deg);
}
<div class="scene">
<div class="panel"></div>
</div>
I tried the setTransform() method without sucess.
function drawScene(margin, size) {
ctx.strokeStyle = "black";
ctx.strokeRect(margin, margin, size, size);
}
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var margin = 100;
var size = 200;
drawScene(margin, size)
ctx.setTransform(1, -0.1, 0, 1, -10, 0);
//ctx.rotate(1 * Math.PI / 180); how to rotateY
ctx.fillStyle = "red";
ctx.fillRect(margin, margin, size, size);
ctx.fillStyle = 'black';
ctx.font = "48px Courier";
ctx.fillText("hello", margin, size);
<canvas id="myCanvas" width="400" height="400" style="border:1px solid #d3d3d3;"></canvas>
I tried both solution from HTML Canvas: Rotate the Image 3D effect but none nailed it. The perspective effet isnt here.

You need create a path by points matrix (x1, y1; x2, y2; x3, y3; x4, y4). Apply a transform matrix in your point matrix. After, print on canvas with path.
1: You need study by tranformation matrix (http://docdingle.com/teaching/cs545/presents/p12b_cs545_WarpsP2.pdf)
2:In Summary you have a transformation' matrix to translading, space, rotate or apply perspective deformation:
//this is a tranformation matrix to percpective deformation A and B are values that apply deformation
matrix_tranf = [ [1, 0 ,0] , [0,1,0] , [ A, B, 1] ];
In my code I left any examples
for(let i = 0; i < 4; i++){
//u
x1 = matrix[i][0]
//v
y1 = matrix[i][1]
w = matrix_tranf[2][0] * x1 + matrix_tranf[2][1] * y1 + 1;
v = [ ( x1 / w ) , ( y1 / w ) ];
matrix[i] = v;
}
Look
https://codepen.io/Luis4raujo/pen/wvoqEwg
If this answer help you, check as correct or voteup!

Related

Merge shapes that share the same border in CSS or JS

I am looking for a simple way to combine shapes via CSS or JS to achieve something like below:
At first I was trying to just overlay the basic shapes using before and after. But by doing so I can not create a border around the whole merged shape. So next I tried a solution using html canvas like:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
ctx.lineWidth = 10;
ctx.strokeRect(30,20,150,250);
ctx.strokeRect(120,160,120,50);
ctx.globalCompositeOperation='destination-out';
//Filling the rects only keeps the border
ctx.fillStyle = "#FF0000";
ctx.fillRect(30,20,150,250);
ctx.fillRect(120,160,120,50);
body{ background-color: green; }
<canvas id="canvas" width=300 height=300></canvas>
But by doing so I wasn't able to keep the custom white fill of the combined shape. So I am now asking for a way to either optimize my current approach to be able to set a custom background for my shape or a completely new approach.
As I need to get the onMouseEnter event I didn't found using a svg-file suitable for my situation.
If there is any framework that would make this process easier, I am also happy to adapt.
I'm not sure this is the most suitable solution for your situation but note that your canvas code can be modified to fill the merged area and detect mouse events just fine.
You don't have to call strokeRect() or fillRect() that do only create a temporary rectangle only for their call. You can create a way more complex path made of several rectangles and other commands. This complex path will then be filled a single entity, doing the "merging" for you. By stroking first and filling after, we can hide the junctions between each shapes.
And to detect if you're over the filled area, you can use ctx.isPointInPath():
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
// beware, we fill over the inner half of the stroke
// so only half of this value will be visible
ctx.lineWidth = 20;
let mouse = { x: -1, y: -1 };
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.rect(30,20,150,250);
ctx.rect(120,160,120,50);
ctx.rect(350,130,50,130);
// contrarily to rect(),
// arc() doesn't call automatically moveTo()
// so to avoid having a link between the previous point
// and the beginning of the arc, we hav to call it ourselves
// this would also be true with any other command, except roundRect()
ctx.moveTo(375+70, 80);
ctx.arc(375, 80, 70, 0, Math.PI*2);
// check if the mouse is over either the stroke or the fill area
const hovering = ctx.isPointInStroke(mouse.x, mouse.y) ||
ctx.isPointInPath(mouse.x, mouse.y)
// change the color based on hover state
ctx.fillStyle = hovering ? "#FF0000" : "#FFFF00";
// It's all a single path
ctx.stroke(); // stroking would show all the various sections
ctx.fill(); // but since we fill over it, we only keep the common outline
}
draw();
// update the hovering state
addEventListener("mousemove", (evt) => {
const rect = canvas.getBoundingClientRect();
mouse.x = evt.clientX - rect.left;
mouse.y = evt.clientY - rect.top;
draw();
});
body{ background-color: green; }
<canvas id="canvas" width=600 height=300></canvas>
And if you need you could separate each shape in their own Path2D object:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
ctx.lineWidth = 20;
let mouse = { x: -1, y: -1 };
const p1 = new Path2D();
p1.rect(30,20,150,250);
p1.rect(120,160,120,50);
const p2 = new Path2D();
p2.rect(350,130,50,130);
p2.moveTo(375+70, 80);
p2.arc(375, 80, 70, 0, Math.PI*2);
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
const overP1 = ctx.isPointInStroke(p1, mouse.x, mouse.y) ||
ctx.isPointInPath(p1, mouse.x, mouse.y)
ctx.fillStyle = overP1 ? "#FF0000" : "#FFFF00";
ctx.stroke(p1);
ctx.fill(p1);
const overP2 = ctx.isPointInStroke(p2, mouse.x, mouse.y) ||
ctx.isPointInPath(p2, mouse.x, mouse.y)
ctx.fillStyle = overP2 ? "#FF0000" : "#FFFF00";
ctx.stroke(p2);
ctx.fill(p2);
}
draw();
addEventListener("mousemove", (evt) => {
const rect = canvas.getBoundingClientRect();
mouse.x = evt.clientX - rect.left;
mouse.y = evt.clientY - rect.top;
draw();
});
body{ background-color: green; }
<canvas id="canvas" width=600 height=300></canvas>
Let's reduce the problem. Build the shapes from atomic shapes such as rectangles and circles. Then you can extrapolate for many shapes. At first I thought about canvas using this function to detect mouse over:
const pointInRect = ({x1, y1, x2, y2}, {x, y}) => (
(x > x1 && x < x2) && (y > y1 && y < y2)
)
But then again, since we reduced the problem we might as well use HTML. Since we are dealing with multiple shapes, it will be easier using some 2D map.
var shape = [
" 1 1111 ".split(""),
"11 1 1 ".split(""),
" 111 111".split(""),
" 1 1 1".split(""),
" 1111 1 ".split(""),
]
const square_size = 40;
var container = document.querySelector(".container");
function draw_square(x, y) {
var div = document.createElement("div");
div.classList.add("square")
div.style.left = x + "px"
div.style.top = y + "px"
container.appendChild(div)
return div;
}
function draw_shape() {
for (var i = 0; i < shape.length; i++) {
for (var j = 0; j < shape[i].length; j++) {
var pixel = shape[i][j];
if (pixel == 1) {
var div = draw_square(j * square_size, i * square_size);
if (i > 0 && shape[i - 1][j] == 1) {
div.style.borderTopColor = "transparent";
}
if (i < shape.length - 1 && shape[i + 1][j] == 1) {
div.style.borderBottomColor = "transparent";
}
if (j > 0 && shape[i][j - 1] == 1) {
div.style.borderLeftColor = "transparent";
}
if (j < shape[i].length - 1 && shape[i][j + 1] == 1) {
div.style.borderRightColor = "transparent";
}
}
}
}
}
draw_shape(shape)
attach_mouse_events()
function attach_mouse_events() {
var squares = document.querySelectorAll(".square");
squares.forEach(function (square) {
square.addEventListener('mouseenter', function(ev) {
squares.forEach(function (square) {
square.classList.add("active");
})
})
square.addEventListener('mouseleave', function(ev) {
squares.forEach(function (square) {
square.classList.remove("active");
})
})
})
}
body {
background: lightgray;
}
.square {
width: 40px;
height: 40px;
background: green;
border: 2px solid black;
position: absolute;
}
.square.active {
background: white;
}
.container {
margin: 20px;
background-size:cover;
height: 300px;
position: relative;
}
<div class="container">
</div>
As for the circles - it's the same idea. I would recommend painting the circles first then the squares, for a perfect borders. Good luck.

How to calculate the offset to draw to the nearest pixel in a canvas with scale transformation applied?

I am trying to draw a rectangle on an HTML canvas with pixel-perfect positioning, irregardless of the translation and scale applied to the canvas transform (assume rotation isn't used in this scenario).
In this case, I always want the border of the rectangle to be 1px wide on the screen, irregardless of how zoomed in the rectangle is with the scale applied (which I have working in the demo below), however I also want it drawn at exact pixel coordinates so no antialiasing is applied.
In the code below, I'm pretty sure I have to manipulate the arguments of ctx.rect to add offsets to each position in order to round it to display exactly on the nearest pixel on screen. But I'm not sure what math to use to get there?
As you can see in this demo, with 1.5 scaling applied the rectangle is no longer drawn on pixel perfect coordinates.
const canvases = [
{
ctx: document.getElementById('canvasOriginal').getContext('2d'),
scale: 1,
translateX: 0,
translateY: 0
},
{
ctx: document.getElementById('canvasZoomed').getContext('2d'),
scale: 1.5,
translateX: -0.5,
translateY: -0.5
}
];
for (const { ctx, scale, translateX, translateY } of canvases) {
ctx.translate(translateX, translateY);
ctx.scale(scale, scale);
ctx.beginPath();
ctx.rect(1.5, 1.5, 4, 4);
ctx.lineWidth = 1 / scale;
ctx.strokeStyle = 'red';
ctx.stroke();
}
canvas {
border: 1px solid #ccc;
image-rendering: pixelated;
image-rendering: crisp-edges;
width: 100px;
height: 100px;
}
<canvas id="canvasOriginal" width="10" height="10"></canvas>
<canvas id="canvasZoomed" width="10" height="10"></canvas>
This is my desired result of the scaled image in the snippet above:
EDIT: Please do not ignore translation.
The given answer has a lot of under the hood overhead, getting and creating a DOM matrix, creating a DOM point, transforming the point then inverting the matrix and transforming back is a literal 100+ multiplications and additions (ignoring the memory management overhead).
As you are only scaling and with no translation or rotation it can be done in one division, eight multiplies and eight additions/subtractions, and be at least an order of magnitude quicker.
Example
const canvases = [
{ ctx: document.getElementById('canvasOriginal').getContext('2d'), scale: 1 },
{ ctx: document.getElementById('canvasZoomed').getContext('2d'), scale: 1.5 },
];
function pathRectPixelAligned(ctx, scale, x, y, w, h) {
const invScale = 1 / scale;
x = (Math.round(x * scale) + 0.5) * invScale ;
y = (Math.round(y * scale) + 0.5) * invScale ;
w = (Math.round((x + w) * scale) - 0.5) * invScale - x;
h = (Math.round((y + h) * scale) - 0.5) * invScale - y;
ctx.rect(x, y, w, h);
}
for (const { ctx, scale } of canvases) {
ctx.scale(scale, scale);
ctx.beginPath();
pathRectPixelAligned(ctx, scale, 1, 1, 4, 4)
ctx.lineWidth = 1;
ctx.strokeStyle = 'red';
ctx.setTransform(1,0,0,1,0,0);
ctx.stroke();
}
canvas {
border: 1px solid #ccc;
image-rendering: pixelated;
image-rendering: crisp-edges;
width: 100px;
height: 100px;
}
<canvas id="canvasOriginal" width="10" height="10"></canvas>
<canvas id="canvasZoomed" width="10" height="10"></canvas>
Ok, I uh... figured it out myself. You can use the DOMPoint class to transform a coordinate through the canvas's transform matrix. So I wrote a function that transforms the point through the matrix, rounds it to the nearest half pixel (since a 1 pixel wide stroke is rendered at the center, e.g. half-point of a pixel), then transforms it back through the inverse of the matrix.
This results in rendering the scaled 1px stroke to the nearest pixel on screen.
Hopefully this question will be useful to others browsing the internet, as it took me forever to figure out this problem prior to posting this question...
const canvases = [
{
ctx: document.getElementById('canvasOriginal').getContext('2d'),
scale: 1,
translateX: 0,
translateY: 0
},
{
ctx: document.getElementById('canvasZoomed').getContext('2d'),
scale: 1.5,
translateX: -0.5,
translateY: -0.5
}
];
const roundPointToHalfIdentityCoordinates = (ctx, x, y) => {
let point = new DOMPoint(x, y);
point = point.matrixTransform(ctx.getTransform());
point.x = Math.round(point.x - 0.5) + 0.5;
point.y = Math.round(point.y - 0.5) + 0.5;
point = point.matrixTransform(ctx.getTransform().inverse());
return point;
};
for (const { ctx, scale, translateX, translateY } of canvases) {
ctx.translate(translateX, translateY);
ctx.scale(scale, scale);
ctx.beginPath();
const topLeft = roundPointToHalfIdentityCoordinates(ctx, 1.5, 1.5);
const bottomRight = roundPointToHalfIdentityCoordinates(ctx, 5.5, 5.5);
ctx.rect(
topLeft.x,
topLeft.y,
bottomRight.x - topLeft.x,
bottomRight.y - topLeft.y
);
ctx.lineWidth = 1 / scale;
ctx.strokeStyle = 'red';
ctx.stroke();
}
canvas {
border: 1px solid #ccc;
image-rendering: pixelated;
image-rendering: crisp-edges;
width: 100px;
height: 100px;
}
<canvas id="canvasOriginal" width="10" height="10"></canvas>
<canvas id="canvasZoomed" width="10" height="10"></canvas>

Canvas: create semi-transparent overlay over entire canvas except one window

I have a rectangular canvas with an image painted on it. As the user moves the cursor over the canvas, I'd like to make the canvas semitransparent except for a small rectangle around the cursor, which I'd like to retain the underlying image content in full opacity.
So my question in brief is how to "mute" the canvas except for a small rectangle.
My first thought was to create a semitransparent overlay over the entire canvas, then just paint the rectangular region that should be full opacity again, but this makes the background disappear while I want to retain it at 0.2 opacity:
var elem = document.querySelector('canvas');
var ctx = elem.getContext('2d');
elem.width = 400;
elem.height = 300;
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, elem.width, elem.height);
elem.addEventListener('mousemove', function(e) {
ctx.globalAlpha = 0.2;
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, elem.width, elem.height);
var x = e.clientX;
var y = e.clientY;
var d = 30;
ctx.fillStyle = '#ff0000';
ctx.fillRect(x-d, y-d, d*2, d*2);
})
<canvas/>
Does anyone know the most performant way to mute the background to 0.2 opacity while retaining the rectangle around the cursor at full opacity? Any pointers would be super helpful!
Here's the two canvas method:
var elemA = document.querySelector('#a');
var elemB = document.querySelector('#b');
var ctx = elemA.getContext('2d');
var bctx = elemB.getContext('2d');
elemA.width = elemB.width = 400;
elemA.height = elemB.height = 300;
ctx.fillStyle = '#ff0000';
ctx.fillRect(0, 0, elemA.width, elemA.height);
elemB.addEventListener('mousemove', function(e) {
bctx.clearRect(0, 0, elemB.width, elemB.height);
var x = e.clientX;
var y = e.clientY;
var x0 = x-10;
var x1 = x+10;
var y0 = y-10;
var y1 = y+10;
// draw boxes; origin is top left
bctx.globalAlpha = 0.8;
bctx.fillStyle = '#ffffff';
bctx.fillRect(0, 0, elemA.width, y0); // top
bctx.fillRect(0, y0, x0, elemB.height+20); // left
bctx.fillRect(x0, y1, elemB.width+20, elemB.height); // bottom
bctx.fillRect(x1, y0, elemA.width, y1-y0); // right
})
* {
margin: 0;
}
#c {
position: relative;
}
#a, #b {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
#b {
z-index: 1;
opacity: 0.5;
}
<div id='c'>
<canvas id='a'></canvas>
<canvas id='b'></canvas>
</div>

Draw dashed and dotted rectangles on canvas in the same way css border works: draw 4 same edges

My use-case is to mimic css border rendering. Is it possible, using the CanvasRenderingContext2D::rect method with CanvasRenderingContext2D::setLineDash to simulate same border drawing as css renderer does, like border: 5px dashed red. Consider this example:
let canvas = document.querySelector('canvas');
let ctx = canvas.getContext('2d');
ctx.lineWidth = 5
ctx.strokeStyle = 'red'
ctx.lineCap = 'square'
ctx.setLineDash([10, 10]);
ctx.beginPath();
ctx.moveTo(2.5,2.5);
ctx.rect(2.5, 2.5, 195, 65);
ctx.stroke();
div {
border: 5px dashed red;
width: 200px;
height: 70px;
box-sizing: border-box;
margin-bottom: 5px;
}
canvas {
display: block;
width: 200px;
height: 70px;
}
<div></div>
<canvas width=200 height=70></canvas>
You may notice the problem is on edges.
I was trying to modify the gaps and dash sizes, but it seems impossible to get the same behaviour as in the css example: the lines on edges are bigger then the lines on the sides. As a workaround I can imagine to draw every side with a line, but I would like to use the rect method to draw in one stroke.
Thank you in advance.
CSS border-style: dashed algorithm is not tied by specs, so it will be impossible to render exactly the same in the canvas API.
Then, you've got to know that even CSS renders it line by line: border is a shorthand for all the border-top-XXX, border-right-XXX, border-bottom-XXX, border-left-XXX.
And that's why it behaves like that : each border has its line-dash set independently of the others.
Anyway, if you want to do it with the canvas API, the easiest solution is to do the same, using four lines, and setting their line-dash separately.
Here is a rough attempt at normalizing the dashes in order to get them always start and end at edges:
var ctx = c.getContext('2d');
ctx.lineCap = 'square';
// returns a normalized dashArray per segment
// This in no way does the same as any browser's implementation,
// this is just a lazy way to always get dashes start and end at edges
function getLineDash(x1, y1, x2, y2) {
var length = Math.hypot((x2 - x1), (y2 - y1));
var dash_length = length / 8;
var nb_of_dashes = length / dash_length;
var dash_gap = (dash_length * 0.66);
dash_length -= dash_gap * 0.33;
return [dash_length, dash_gap];
}
function draw() {
ctx.lineWidth = lineWidth_.value;
ctx.clearRect(0, 0, c.width, c.height);
var points = [
[x1_.value, y1_.value],
[x2_.value, y2_.value],
[x3_.value, y3_.value],
[x4_.value, y4_.value]
];
points.forEach(function(pt, i) {
var next = points[(i + 1) % points.length];
ctx.beginPath();
ctx.moveTo(pt[0], pt[1]);
ctx.lineTo(next[0], next[1]);
ctx.setLineDash(getLineDash(pt[0], pt[1], next[0], next[1]));
ctx.stroke();
});
}
draw();
document.oninput = function(e) {
if (e.target.parentNode.parentNode === inputs_) {
draw();
}
}
label {
display: inline-block;
}
input {
max-width: 50px;
}
<div id="inputs_">
<label>x1<input type="number" id="x1_" value="10"></label>
<label>y1<input type="number" id="y1_" value="25"></label>
<label>x2<input type="number" id="x2_" value="350"></label>
<label>y2<input type="number" id="y2_" value="25"></label>
<label>x3<input type="number" id="x3_" value="350"></label>
<label>y3<input type="number" id="y3_" value="225"></label>
<label>x4<input type="number" id="x4_" value="10"></label>
<label>y4<input type="number" id="y4_" value="225"></label>
<label>lineWidth<input type="number" id="lineWidth_" value="3"></label>
</div>
<canvas id="c" width="400" height="400"></canvas>
So now, if you only want to use XXXRect, you can also create a single huge dash-array containing all of the dashes...
var ctx = c.getContext('2d');
ctx.lineCap = 'square';
function getRectDashes(width, height) {
var w_array = getLineDashes(width, 0, 0, 0);
var h_array = getLineDashes(0, height, 0, 0);
dashArray = [].concat.apply([], [w_array, 0, h_array, 0, w_array, 0, h_array]);
return dashArray;
}
// same as previous snippet except that it does return all the segment's dashes
function getLineDashes(x1, y1, x2, y2) {
var length = Math.hypot((x2 - x1), (y2 - y1));
var dash_length = length / 8;
var nb_of_dashes = length / dash_length;
var dash_gap = dash_length * 0.66666;
dash_length -= dash_gap * 0.3333;
var total_length = 0;
var dasharray = [];
var next;
while (total_length < length) {
next = dasharray.length % 2 ? dash_gap : dash_length;
total_length += next;
dasharray.push(next);
}
return dasharray;
}
function draw() {
ctx.clearRect(0, 0, c.width, c.height);
ctx.lineWidth = lineWidth_.value;
var w = width_.value,
h = height_.value;
ctx.setLineDash(getRectDashes(w, h));
ctx.strokeRect(20, 20, w, h);
}
draw();
document.oninput = function(e) {
if (e.target.parentNode.parentNode === inputs_)
draw();
};
label {
display: inline-block;
}
input {
max-width: 50px;
}
<div id="inputs_">
<label>width<input type="number" id="width_" value="200"></label>
<label>height<input type="number" id="height_" value="225"></label>
<label>lineWidth<input type="number" id="lineWidth_" value="3"></label>
</div>
<canvas id="c" width="400" height="400"></canvas>

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.
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>
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>
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>

Categories