I Have to draw Herringbone pattern on canvas and fill with image
some one please help me I am new to canvas 2d drawing.
I need to draw mixed tiles with cross pattern (Herringbone)
var canvas = this.__canvas = new fabric.Canvas('canvas');
var canvas_objects = canvas._objects;
// create a rectangle with a fill and a different color stroke
var left = 150;
var top = 150;
var x=20;
var y=40;
var rect = new fabric.Rect({
left: left,
top: top,
width: x,
height: y,
angle:45,
fill: 'rgba(255,127,39,1)',
stroke: 'rgba(34,177,76,1)',
strokeWidth:0,
originX:'right',
originY:'top',
centeredRotation: false
});
canvas.add(rect);
for(var i=0;i<15;i++){
var rectangle = fabric.util.object.clone(getLastobject());
if(i%2==0){
rectangle.left = rectangle.oCoords.tr.x;
rectangle.top = rectangle.oCoords.tr.y;
rectangle.originX='right';
rectangle.originY='top';
rectangle.angle =-45;
}else{
fabric.log('rectangle: ', rectangle.toJSON());
rectangle.left = rectangle.oCoords.tl.x;
rectangle.top = rectangle.oCoords.tl.y;
fabric.log('rectangle: ', rectangle.toJSON());
rectangle.originX='left';
rectangle.originY='top';
rectangle.angle =45;
}
//rectangle.angle -90;
canvas.add(rectangle);
}
fabric.log('rectangle: ', canvas.toJSON());
canvas.renderAll();
function getLastobject(){
var last = null;
if(canvas_objects.length !== 0){
last = canvas_objects[canvas_objects.length -1]; //Get last object
}
return last;
}
How to draw this pattern in canvas using svg or 2d,3d method. If any third party library that also Ok for me.
I don't know where to start and how to draw this complex pattern.
some one please help me to draw this pattern with rectangle fill with dynamic color on canvas.
Here is a sample of the output I need: (herringbone pattern)
I tried something similar using fabric.js library here is my JSFiddle
Trippy disco flooring
To get the pattern you need to draw rectangles one horizontal tiled one space left or right for each row down and the same for the vertical rectangle.
The rectangle has an aspect of width 2 time height.
Drawing the pattern is simple.
Rotating is easy as well the harder part is finding where to draw the tiles for the rotation.
To do that I create a inverse matrix of the rotation (it reverses a rotation). I then apply that rotation to the 4 corners of the canvas 0,0, width,0 width,height and 0,height this gives me 4 points in the rotated space that are at the edges of the canvas.
As I draw the tiles from left to right top to bottom I find the min corners for the top left, and the max corners for the bottom right, expand it out a little so I dont miss any pixels and draw the tiles with a transformation set the the rotation.
As I could not workout what angle you wanted it at the function will draw it at any angle. On is animated, the other is at 60deg clockwise.
Warning demo contains flashing content.
Update The flashing was way to out there, so have made a few changes, now colours are a more pleasing blend and have fixed absolute positions, and have tied the tile origin to the mouse position, clicking the mouse button will cycle through some sizes as well.
const ctx = canvas.getContext("2d");
const colours = []
for(let i = 0; i < 1; i += 1/80){
colours.push(`hsl(${Math.floor(i * 360)},${Math.floor((Math.sin(i * Math.PI *4)+1) * 50)}%,${Math.floor(Math.sin(i * Math.PI *8)* 25 + 50)}%)`)
}
const sizes = [0.04,0.08,0.1,0.2];
var currentSize = 0;
const origin = {x : canvas.width / 2, y : canvas.height / 2};
var size = Math.min(canvas.width * 0.2, canvas.height * 0.2);
function drawPattern(size,origin,ang){
const xAx = Math.cos(ang); // define the direction of xAxis
const xAy = Math.sin(ang);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.setTransform(xAx,xAy,-xAy,xAx,origin.x,origin.y);
function getExtent(xAx,xAy,origin){
const im = [1,0,0,1]; // inverse matrix
const dot = xAx * xAx + xAy * xAy;
im[0] = xAx / dot;
im[1] = -xAy / dot;
im[2] = xAy / dot;
im[3] = xAx / dot;
const toWorld = (x,y) => {
var point = {};
var xx = x - origin.x;
var yy = y - origin.y;
point.x = xx * im[0] + yy * im[2];
point.y = xx * im[1] + yy * im[3];
return point;
}
return [
toWorld(0,0),
toWorld(canvas.width,0),
toWorld(canvas.width,canvas.height),
toWorld(0,canvas.height),
]
}
const corners = getExtent(xAx,xAy,origin);
var startX = Math.min(corners[0].x,corners[1].x,corners[2].x,corners[3].x);
var endX = Math.max(corners[0].x,corners[1].x,corners[2].x,corners[3].x);
var startY = Math.min(corners[0].y,corners[1].y,corners[2].y,corners[3].y);
var endY = Math.max(corners[0].y,corners[1].y,corners[2].y,corners[3].y);
startX = Math.floor(startX / size) - 2;
endX = Math.floor(endX / size) + 2;
startY = Math.floor(startY / size) - 2;
endY = Math.floor(endY / size) + 2;
// draw the pattern
ctx.lineWidth = size * 0.1;
ctx.lineJoin = "round";
ctx.strokeStyle = "black";
var colourIndex = 0;
for(var y = startY; y <endY; y+=1){
for(var x = startX; x <endX; x+=1){
if((x + y) % 4 === 0){
colourIndex = Math.floor(Math.abs(Math.sin(x)*size + Math.sin(y) * 20));
ctx.fillStyle = colours[(colourIndex++)% colours.length];
ctx.fillRect(x * size,y * size,size * 2,size);
ctx.strokeRect(x * size,y * size,size * 2,size);
x += 2;
ctx.fillStyle = colours[(colourIndex++)% colours.length];
ctx.fillRect(x * size,y * size, size, size * 2);
ctx.strokeRect(x * size,y * size, size, size * 2);
x += 1;
}
}
}
}
// Animate it all
var update = true; // flag to indecate something needs updating
function mainLoop(time){
// if window size has changed update canvas to new size
if(canvas.width !== innerWidth || canvas.height !== innerHeight || update){
canvas.width = innerWidth;
canvas.height = innerHeight
origin.x = canvas.width / 2;
origin.y = canvas.height / 2;
size = Math.min(canvas.width, canvas.height) * sizes[currentSize % sizes.length];
update = false;
}
if(mouse.buttonRaw !== 0){
mouse.buttonRaw = 0;
currentSize += 1;
update = true;
}
// draw the patter
drawPattern(size,mouse,time/2000);
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
mouse = (function () {
function preventDefault(e) { e.preventDefault() }
var m; // alias for mouse
var mouse = {
x : 0, y : 0, // mouse position
buttonRaw : 0,
over : false, // true if mouse over the element
buttonOnMasks : [0b1, 0b10, 0b100], // mouse button on masks
buttonOffMasks : [0b110, 0b101, 0b011], // mouse button off masks
bounds : null,
eventNames : "mousemove,mousedown,mouseup,mouseout,mouseover".split(","),
event(e) {
var t = e.type;
m.bounds = m.element.getBoundingClientRect();
m.x = e.pageX - m.bounds.left - scrollX;
m.y = e.pageY - m.bounds.top - scrollY;
if (t === "mousedown") { m.buttonRaw |= m.buttonOnMasks[e.which - 1] }
else if (t === "mouseup") { m.buttonRaw &= m.buttonOffMasks[e.which - 1] }
else if (t === "mouseout") { m.over = false }
else if (t === "mouseover") { m.over = true }
e.preventDefault();
},
start(element) {
if (m.element !== undefined) { m.remove() }
m.element = element === undefined ? document : element;
m.eventNames.forEach(name => document.addEventListener(name, mouse.event) );
document.addEventListener("contextmenu", preventDefault, false);
},
}
m = mouse;
return mouse;
})();
mouse.start(canvas);
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
Un-animated version at 60Deg
const ctx = canvas.getContext("2d");
const colours = ["red","green","yellow","orange","blue","cyan","magenta"]
const origin = {x : canvas.width / 2, y : canvas.height / 2};
var size = Math.min(canvas.width * 0.2, canvas.height * 0.2);
function drawPattern(size,origin,ang){
const xAx = Math.cos(ang); // define the direction of xAxis
const xAy = Math.sin(ang);
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.setTransform(xAx,xAy,-xAy,xAx,origin.x,origin.y);
function getExtent(xAx,xAy,origin){
const im = [1,0,0,1]; // inverse matrix
const dot = xAx * xAx + xAy * xAy;
im[0] = xAx / dot;
im[1] = -xAy / dot;
im[2] = xAy / dot;
im[3] = xAx / dot;
const toWorld = (x,y) => {
var point = {};
var xx = x - origin.x;
var yy = y - origin.y;
point.x = xx * im[0] + yy * im[2];
point.y = xx * im[1] + yy * im[3];
return point;
}
return [
toWorld(0,0),
toWorld(canvas.width,0),
toWorld(canvas.width,canvas.height),
toWorld(0,canvas.height),
]
}
const corners = getExtent(xAx,xAy,origin);
var startX = Math.min(corners[0].x,corners[1].x,corners[2].x,corners[3].x);
var endX = Math.max(corners[0].x,corners[1].x,corners[2].x,corners[3].x);
var startY = Math.min(corners[0].y,corners[1].y,corners[2].y,corners[3].y);
var endY = Math.max(corners[0].y,corners[1].y,corners[2].y,corners[3].y);
startX = Math.floor(startX / size) - 4;
endX = Math.floor(endX / size) + 4;
startY = Math.floor(startY / size) - 4;
endY = Math.floor(endY / size) + 4;
// draw the pattern
ctx.lineWidth = 5;
ctx.lineJoin = "round";
ctx.strokeStyle = "black";
for(var y = startY; y <endY; y+=1){
for(var x = startX; x <endX; x+=1){
ctx.fillStyle = colours[Math.floor(Math.random() * colours.length)];
if((x + y) % 4 === 0){
ctx.fillRect(x * size,y * size,size * 2,size);
ctx.strokeRect(x * size,y * size,size * 2,size);
x += 2;
ctx.fillStyle = colours[Math.floor(Math.random() * colours.length)];
ctx.fillRect(x * size,y * size, size, size * 2);
ctx.strokeRect(x * size,y * size, size, size * 2);
x += 1;
}
}
}
}
canvas.width = innerWidth;
canvas.height = innerHeight
origin.x = canvas.width / 2;
origin.y = canvas.height / 2;
size = Math.min(canvas.width * 0.2, canvas.height * 0.2);
drawPattern(size,origin,Math.PI / 3);
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
The best way to approach this is to examine the pattern and analyse its symmetry and how it repeats.
You can look at this several ways. For example, you could rotate the patter 45 degrees so that the tiles are plain orthogonal rectangles. But let's just look at it how it is. I am going to assume you are happy with it with 45deg tiles.
Like the tiles themselves, it turns out the pattern has a 2:1 ratio. If we repeat this pattern horizontally and vertically, we can fill the canvas with the completed pattern.
We can see there are five tiles that overlap with our pattern block. However we don't need to draw them all when we draw each pattern block. We can take advantage of the fact that blocks are repeated, and we can leave the drawing of some tiles to later rows and columns.
Let's assume we are drawing the pattern blocks from left to right and top to bottom. Which tiles do we need to draw, at a minimum, to ensure this pattern block gets completely drawn (taking into account adjacent pattern blocks)?
Since we will be starting at the top left (and moving right and downwards), we'll need to draw tile 2. That's because that tile won't get drawn by either the block below us, or the block to the right of us. The same applies to tile 3.
It turns out those two are all we'll need to draw for each pattern block. Tile 1 and 4 will be drawn when the pattern block below us draws their tile 2 and 3 respectively. Tile 5 will be drawn when the pattern block to the south-east of us draws their tile 1.
We just need to remember that we may need to draw an extra column on the right-hand side, and at the bottom, to ensure those end-of-row and end-of-column pattern blocks get completely drawn.
The last thing to work out is how big our pattern blocks are.
Let's call the short side of the tile a and the long side b. We know that b = 2 * a. And we can work out, using Pythagoras Theorem, that the height of the pattern block will be:
h = sqrt(a^2 + a^2)
= sqrt(2 * a^2)
= sqrt(2) * a
The width of the pattern block we can see will be w = 2 * h.
Now that we've worked out how to draw the pattern, let's implement our algorithm.
const a = 60;
const b = 120;
const h = 50 * Math.sqrt(2);
const w = h * 2;
const h2 = h / 2; // How far tile 1 sticks out to the left of the pattern block
// Set of colours for the tiles
const colours = ["red","cornsilk","black","limegreen","deepskyblue",
"mediumorchid", "lightgrey", "grey"]
const canvas = document.getElementById("herringbone");
const ctx = canvas.getContext("2d");
// Set a universal stroke colour and width
ctx.strokeStyle = "black";
ctx.lineWidth = 4;
// Loop through the pattern block rows
for (var y=0; y < (canvas.height + h); y+=h)
{
// Loop through the pattern block columns
for (var x=0; x < (canvas.width + w); x+=w)
{
// Draw tile "2"
// I'm just going to draw a path for simplicity, rather than
// worrying about drawing a rectangle with rotation and translates
ctx.beginPath();
ctx.moveTo(x - h2, y - h2);
ctx.lineTo(x, y - h);
ctx.lineTo(x + h, y);
ctx.lineTo(x + h2, y + h2);
ctx.closePath();
ctx.fillStyle = colours[Math.floor(Math.random() * colours.length)];
ctx.fill();
ctx.stroke();
// Draw tile "3"
ctx.beginPath();
ctx.moveTo(x + h2, y + h2);
ctx.lineTo(x + w - h2, y - h2);
ctx.lineTo(x + w, y);
ctx.lineTo(x + h, y + h);
ctx.closePath();
ctx.fillStyle = colours[Math.floor(Math.random() * colours.length)];
ctx.fill();
ctx.stroke();
}
}
<canvas id="herringbone" width="500" height="400"></canvas>
Related
So I have created an html canvas with the illusion of an infinite grid. My goal was to make it as efficient as possible, drawing only what is visible and simulating all the effects by doing the math, instead of for example drawing the grid 3-times as wide and high to make it "infinite". I implemented it by storing the x- and y-offset of the grid. When the mouse moves, the offset is being increased by the moved distance, and then clamped to the cell size. This way, when the moved distance is bigger than the size of one cell, the offset starts again at 0, because only the "overlapping distance" needs to be drawn. This way I can create the illusion of an infinite grid without actually having to worry about world coordinates etc. The snippet below is a working version of this:
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
let width = 200;
let height = 200;
let dpi = 4;
let cellSize = 10;
let pressed = false;
canvas.height = height * dpi;
canvas.width = width * dpi;
canvas.style.height = height + "px";
canvas.style.width = width + "px";
canvas.addEventListener("mousedown", (e) => mousedown(e));
canvas.addEventListener("mouseup", (e) => mouseup(e));
canvas.addEventListener("mousemove", (e) => mousemove(e));
let offset = {x: 0, y: 0};
draw();
function draw() {
ctx.save();
ctx.scale(dpi, dpi);
ctx.translate(-0.5, -0.5);
ctx.lineWidth = 1;
ctx.strokeStyle = "silver";
ctx.beginPath();
for (let x = offset.x; x < width; x += cellSize) {
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
}
for (let y = offset.y; y < height; y += cellSize) {
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
ctx.closePath();
ctx.stroke();
ctx.restore();
}
function mousedown(e) {
pressed = true;
}
function mouseup(e) {
pressed = false;
}
function mousemove(e) {
if (!pressed) {
return;
}
ctx.clearRect(0, 0, width * dpi, height * dpi);
offset.x += e.movementX;
offset.y += e.movementY;
let signX = offset.x > 0 ? 1 : -1;
let signY = offset.y > 0 ? 1 : -1;
offset = {
x: (Math.abs(offset.x) > cellSize)
? offset.x - Math.floor((offset.x * signX) / cellSize) * cellSize * signX
: offset.x,
y: (Math.abs(offset.y) > cellSize)
? offset.y - Math.floor((offset.y * signY) / cellSize) * cellSize * signY
: offset.y
};
draw();
}
canvas {
background-color: white;
}
<canvas></canvas>
I now wanted to implement zooming into the illusion. This could be achieved by increasing the cell size according to the zoom level. I also changed the way the grid was drawn: Instead of clamping the offset, and beginning to draw the lines at that offset, the offset is now the center of the grid (thats why its starting position is at w/2 and h/2). I then draw the lines from the offset to the left, top, bottom and right edge. This allows me, when zooming, to simply set the offset to the mouse position, and increase the cell size. This way it looks like it would zoom to the mouse position, because the cell size increases "away" from that point. This works fine and looks pretty nice so far, try it out below:
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
let width = 200;
let height = 200;
let dpi = 4;
let cellSize = 10;
let pressed = false;
let zoomIntensity = 0.1;
let zoom = 1;
canvas.height = height * dpi;
canvas.width = width * dpi;
canvas.style.height = height + "px";
canvas.style.width = width + "px";
canvas.addEventListener("mousedown", (e) => mousedown(e));
canvas.addEventListener("mouseup", (e) => mouseup(e));
canvas.addEventListener("mousemove", (e) => mousemove(e));
canvas.addEventListener("wheel", (e) => wheel(e));
let offset = {x: width / 2, y: height / 2};
draw();
function draw() {
ctx.save();
ctx.scale(dpi, dpi);
ctx.translate(-0.5, -0.5);
ctx.lineWidth = 1;
ctx.strokeStyle = "silver";
ctx.beginPath();
for (let x = offset.x; x < width; x += cellSize * zoom) {
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
}
for (let x = offset.x; x > 0; x -= cellSize * zoom) {
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
}
for (let y = offset.y; y < height; y += cellSize * zoom) {
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
for (let y = offset.y; y > 0; y -= cellSize * zoom) {
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
ctx.closePath();
ctx.stroke();
ctx.fillStyle = "red";
ctx.arc(offset.x, offset.y, 4, 0, 2*Math.PI);
ctx.fill();
ctx.restore();
}
function mousedown(e) {
pressed = true;
}
function mouseup(e) {
pressed = false;
}
function mousemove(e) {
if (!pressed) {
return;
}
offset.x += e.movementX;
offset.y += e.movementY;
let signX = offset.x > 0 ? 1 : -1;
let signY = offset.y > 0 ? 1 : -1;
/*offset = {
x: (Math.abs(offset.x) > cellSize)
? offset.x - Math.floor((offset.x * signX) / cellSize) * cellSize * signX
: offset.x,
y: (Math.abs(offset.y) > cellSize)
? offset.y - Math.floor((offset.y * signY) / cellSize) * cellSize * signY
: offset.y
};*/
update();
}
function update() {
ctx.clearRect(0, 0, width * dpi, height * dpi);
draw();
}
function wheel(e) {
offset = {x: e.offsetX, y: e.offsetY};
zoom += ((e.deltaY > 0) ? -1 : 1) * zoomIntensity;
update();
}
canvas {
background-color: white;
}
<p>Scroll with mouse wheel<p>
<canvas></canvas>
However, as you might have noticed, when changing the mouse position while zooming, the grid looks like it jumps to that position. That is logical, because the new center of the grid is exactly at the mouse position - regardless of the distance of the mouse to the nearest cell. This way, when trying to zoom in on the center of a cell, the new grid creates a line exactly at that center, making it look like it jumped. I tried to store the offset of the mouse position to the nearest cell border and drawing everything shifted by that offset, but I couldnt get it to work. I would need to know when a new wheel event is initiated (like on mousedown) and then store the offset to the nearest cells borders, and draw everything shifted by those offsets * zoom, until the zooming ended. I am having trouble implementing something like that, because there are no inbuilt listener functions for the wheel besides wheel, and using something different like keyboard etc. isnt an option here. It is still my goal to make that illusion as efficient as it can be, trying to avoid ctx.scale() and ctx.translate and rather calculate the coords myself.
I was able to solve it on my own after many hours of trying different mathematical approches. At the end of this answer you will find a working snippet that simulates an infinite, pan- and zoomable grid. Because Im doing all the maths myself and draw only whats visible, the illusion of the infinite grid is really fast and efficient. You might notice that the lines are sometimes blurry when zooming in, this could be solved by rounding the numbers at which the lines are drawn to integers, however then you will notice small "jumps" while zooming in, because the lines are slighty shifted. I will now give my best to try to explain process how I achieved the illusion and explain the mathematical procedures.
Quick notes for the snippet:
zoomPoint (red point) is the point around the grid should be zoomed
rx and ry (blue lines) are the offset from the zoomPoint to the right or bottom border of the nearest cell
the top, left, bottom and right variables are the coordinates, at which the borders of the cell around the zoomPoint should be drawn
How does the zooming work?
on zoom, store the current zoomPoint in lastZoomPoint
update zoomPoint to new position of mouse
store current scale in lastScale
update scale by adding scaleStep * amt where amt is -1 or 1, depending on the wheels scrolling direction (deltaY)
calculate rx and ry (the distances from the new zoomPoint to the nearest vertical or horizontal line). it is important to use lastZoomPoint and lastScale here! Here is a piece of my notes to better understand exactly whats going on(needed for the following explanation):
picture
multiply rx and ry with scale, and draw the new grid
So we want to calculate the new rx (and ry, but once we have a formula for rx we can use that to calculate ry).
P2 (the green point) is our lastZoomPoint. We need the new rx, in the picture its called rneu. To get that, we want to subtract the yellow distance from Pneu. But actually, we can make this simpler. We only need a point that we know is perfectly on any of the vertical lines, lets call it Pv. Because rneu is the distance of Pneu to the next vertical line on its left, we need a point Pnearest, exactly on that vertical line. We can get that Point by just moving Pv by the size of one cell times the amount of cells between the two points. Well, we know the size of one cell (cellSize * lastScale), we now need the number of cells in between Pv and Pnearest. We can get that number by calculating the x-distance of these two points and divide it by the size of one cell. Lets use Math.floor to get the number of whole cells in between. Multiply that number by the size of one cell and add it to Pv and voilĂ , we get Pnearest. Lets write that down mathematically:
let Rneu = Pneu - Pnearest
let Pnearest = Pv + NWholeCells * scaledCellSize
let NWholeCells = Math.floor((Pneu - Pv) / (scaledCellSize))
let scaledCellSize = lastScale * cellSize
Substitute everything in, we get:
let Rneu = Pneu - (Pv + Math.floor((Pneu - Pv) / (lastScale * cellSize)) * (lastScale * cellSize))
So we know Pneu, lastScale and cellSize. The only thing missing is Pv. Remember, this was any Point that lies perfectly on one of the vertical lines. And this is going to be straightforward: Subtract rx (the one from the previous calculation) from P2 and we have ourselves a point that is perfectly on a vertical line!
So:
let Pv = P2 - rx
// substitue that in again
let Rneu = Pneu - ((P2 - rx) + Math.floor((Pneu - (P2 - rx)) / (lastScale * cellSize)) * (lastScale * cellSize));
Nice! Simplify that, and we get:
let Rneu = Pneu - P2 + rx - Math.floor((Pneu - P2 + rx) / (lastScale * cellSize)) * lastScale * cellSize);
In the snippet Pneu - P2 + rx is called dx.
So, now we have the distance from our new zoomPoint to the nearest vertical line. This distance however is obviously scaled by lastScale, so we need to divide rx by lastScale (this will become cleared in further explanation)
Now we have a "cleansed" rx. This "cleansed" value is the distance Pneu to the nearest cell border, if the scale was equal to 1. This is our "initial state". From that (this is now in calculateDrawingPositions()), to make it appear as if we were zooming in on Pneu, we only need to multiple the "cleansed" rx by the new scale (this time not lastScale but scale!). Now we start drawing the vertical lines of our new grid at Pneu - rx * scale, and decrease that x by cellSize * scale, until x reached 0. We now have the vertical lines on the left of our Pneu. Do the same for the horizontal lines above Pneu, and the starting points for the lines on the right or below are easily calculated by adding cellSize * scale to the left or top drawing position.
Puh, done! Thats it! I hope I didnt miss something in my explanation and everything is correct. It took me long to come up with that solution, I hope I explained it in a way it can be easily understood, however I dont know if thats even possible, for me this whole task was pretty complex.
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
let width = 200;
let height = 200;
let dpi = 4;
let cellSize = 10;
let backgroundColor = "white";
let lineColor = "silver";
let pressed = false;
let scaleStep = 0.1;
let scale = 1;
let lastScale = scale;
let maxScale = 10;
let minScale = 0.1;
let zoomPoint = {
x: 0,
y: 0
};
let lastZoomPoint = zoomPoint;
let rx = 0,
ry = 0;
let left = 0,
right = 0,
_top = 0,
bottom = 0;
resizeCanvas();
addEventListeners();
calculate();
draw();
function resizeCanvas() {
canvas.height = height * dpi;
canvas.width = width * dpi;
canvas.style.height = height + "px";
canvas.style.width = width + "px";
}
function addEventListeners() {
canvas.addEventListener("mousedown", (e) => mousedown(e));
canvas.addEventListener("mouseup", (e) => mouseup(e));
canvas.addEventListener("mousemove", (e) => mousemove(e));
canvas.addEventListener("wheel", (e) => wheel(e));
}
function calculate() {
calculateDistancesToCellBorders();
calculateDrawingPositions();
}
function calculateDistancesToCellBorders() {
let dx = zoomPoint.x - lastZoomPoint.x + rx * lastScale;
rx = dx - Math.floor(dx / (lastScale * cellSize)) * lastScale * cellSize;
rx /= lastScale;
let dy = zoomPoint.y - lastZoomPoint.y + ry * lastScale;
ry = dy - Math.floor(dy / (lastScale * cellSize)) * lastScale * cellSize;
ry /= lastScale;
}
function calculateDrawingPositions() {
let scaledCellSize = cellSize * scale;
left = zoomPoint.x - rx * scale;
right = left + scaledCellSize;
_top = zoomPoint.y - ry * scale;
bottom = _top + scaledCellSize;
}
function draw() {
ctx.save();
ctx.scale(dpi, dpi);
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, width, height);
ctx.lineWidth = 1;
ctx.strokeStyle = lineColor;
ctx.translate(-0.5, -0.5);
ctx.beginPath();
let scaledCellSize = cellSize * scale;
for (let x = left; x > 0; x -= scaledCellSize) {
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
}
for (let x = right; x < width; x += scaledCellSize) {
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
}
for (let y = _top; y > 0; y -= scaledCellSize) {
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
for (let y = bottom; y < height; y += scaledCellSize) {
ctx.moveTo(0, y);
ctx.lineTo(width, y);
}
ctx.stroke();
/* Only for understanding */
ctx.strokeStyle = "blue";
ctx.beginPath();
ctx.moveTo(zoomPoint.x, zoomPoint.y);
ctx.lineTo(left, zoomPoint.y);
ctx.moveTo(zoomPoint.x, zoomPoint.y);
ctx.lineTo(zoomPoint.x, _top);
ctx.stroke();
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(zoomPoint.x, zoomPoint.y, 2, 0, 2*Math.PI);
ctx.fill();
/* -----------------------*/
ctx.restore();
}
function update() {
ctx.clearRect(0, 0, width * dpi, height * dpi);
draw();
}
function move(dx, dy) {
zoomPoint.x += dx;
zoomPoint.y += dy;
}
function zoom(amt, point) {
lastScale = scale;
scale += amt * scaleStep;
if (scale < minScale) {
scale = minScale;
}
if (scale > maxScale) {
scale = maxScale;
}
lastZoomPoint = zoomPoint;
zoomPoint = point;
}
function wheel(e) {
zoom(e.deltaY > 0 ? -1 : 1, {
x: e.offsetX,
y: e.offsetY
});
calculate();
update();
}
function mousedown(e) {
pressed = true;
}
function mouseup(e) {
pressed = false;
}
function mousemove(e) {
if (!pressed) {
return;
}
move(e.movementX, e.movementY);
// do not recalculate the distances again, this wil lead to wronrg drawing
calculateDrawingPositions();
update();
}
canvas {
border: 1px solid black;
}
<p>Zoom with wheel, move by dragging</p>
<canvas></canvas>
i have tried this,
public drawNumbers(ctx, x1, y1, length, count) {
let angle = 0;
for (let i = 0; i <= count; i++ ) {
angle += 2 * Math.PI / (count );
const x2 = x1 + length * Math.cos(angle),
y2 = y1 + length * Math.sin(angle);
ctx.beginPath();
ctx.fillRect(x2, y2, 10, 20);
ctx.stroke();
}
}
this.canvas.drawNumbers(ctx, this.midX, this.midY, 160, 60);
output:
expected result:
i want to calculate a four coordinate(rectangle) of rotated axis.
How do i detect click event on each rectangle?
Using setTransform
Salix alba answer is a solution though a few too many steps.
It can be done in a single transform using setTransform and applying the translate and rotations in one step. Also the second translation is where you draw the box relative to its origin. When using transforms always draw objects around the center of rotation.
ctx.strokeRect(-10,-10,20,20); // rotation is always around 0,0
const ctx = canvas.getContext("2d");
const centerX = 250;
const centerY = 250;
const radius = 200;
const boxWidth = 10;
const bobLength = 20;
// draw boxs around circle center at cx,cy and radius rad
// box width bw, and box height bh
// spacing optional is the distance between boxes
function drawCircleOfBoxes(cx,cy,rad,bw,bh,spacing = 5){
var steps = ((rad - bw /2) * Math.PI * 2) / (bw + spacing) | 0; // get number boxes that will fit circle
ctx.beginPath();
for(var i = 0; i < steps; i ++){
const ang = (i / steps) * Math.PI * 2;
var xAxisX = Math.cos(ang); // get the direction of he xAxis
var xAxisY = Math.sin(ang);
// set the transform to circle center x Axis out towards box
// y axis at 90 deg to x axis
ctx.setTransform(xAxisX, xAxisY, -xAxisY, xAxisX, cx, cy);
// draw box offset from the center so its center is distance radius
ctx.rect(rad - bh / 2, -bw / 2, bh, bw);
}
ctx.fill();
ctx.stroke();
ctx.setTransform(1,0,0,1,0,0); // reset transform
}
ctx.fillStyle = "#FCD";
ctx.strokeStyle = "#000";
drawCircleOfBoxes(centerX, centerY, radius, boxWidth, bobLength);
<canvas id="canvas" width="500" height="500"></canvas>
Manually apply the transform to a point
If you wish to transform the box in code you can use the transform applied in the above and apply it directly to a set of points. You can not apply it to the ctx.rect function that needs the API transform.
To transform a point px,py you need the the direction of the rotated x axis
const xAx = Math.cos(dirOfXAxis);
const xAy = Math.sin(dirOfXAxis);
You can then move the point px distance along the xAxis and then turn 90 deg and move py distance along the y axis
var x = px * xAx; // move px dist along x axis
var y = px * xAy;
x += py * -xAy; // move px dist along y axis
y += py * xAx;
Then just add the translation
x += translateX;
y += translateY;
Or done in one go
var x = px * xAx - py * xAy + translateX; // move px dist along x axis
var y = px * xAy + py * xAx + translateY;
The snippet shows it in action
const ctx = canvas.getContext("2d");
const centerX = 250;
const centerY = 250;
const radius = 200;
const boxWidth = 10;
const boxLength = 20;
// draw boxs around circle center at cx,cy and radius rad
// box width bw, and box height bh
// spacing optional is the distance between boxes
function drawCircleOfBoxes(cx,cy,rad,bw,bh,spacing = 5){
var points = [ // setout points of box with coord (0,0) as center
{x : bh / 2, y : -bw / 2},
{x : bh / 2 + bh, y : -bw / 2},
{x : bh / 2 + bh, y : -bw / 2 + bw},
{x : bh / 2, y : -bw / 2 + bw},
];
var steps = (((rad - bw /2) * Math.PI * 2) / (bw + spacing) )+ 4| 0; // get number boxes that will fit circle
ctx.beginPath();
for(var i = 0; i < steps; i ++){
const ang = (i / steps) * Math.PI * 2;
const xAx = Math.cos(ang); // get the direction of he xAxis
const xAy = Math.sin(ang);
var first = true
for(const p of points){ // for each point
// Apply the transform to the point after moving it
// to the circle (the p.x + rad)
const x = (p.x + rad) * xAx - p.y * xAy + cx;
const y = (p.x + rad) * xAy + p.y * xAx + cy;
if(first){
ctx.moveTo(x,y);
first = false;
}else{
ctx.lineTo(x,y);
}
}
ctx.closePath();
}
ctx.fill();
ctx.stroke();
}
ctx.fillStyle = "#CFD";
ctx.strokeStyle = "#000";
for(var i = boxLength + 5; i < radius; i += boxLength + 5){
drawCircleOfBoxes(centerX, centerY, i , boxWidth, boxLength);
}
<canvas id="canvas" width="500" height="500"></canvas>
To get rotated rectangles you need to use the transform() method of the graphics context.
Imagine a set of axis at the top left of the drawing area. Any drawing will be done relative to these axis which we can move with transform.
To translate by xshift, yshift
ctx.transform(1,0,0,1, xshift, yshift);
ctx.fillRect(0,0,100,100);
To rotate by angle ang in radians
ctx.transform(Math.cos(ang),Math.sin(ang),
-Math.sin(ang),Math.cos(ang), 0,0);
We can combine things with three transformations. The first moves the origin to the center of the circle. Then rotate the axes about this point,
then shift the axes to where you want the shape to appear. Finally, draw the shape.
for(deg = 0; deg < 360; deg+=20) {
ctx.setTransform(1,0,0,1,0,0); // reset transformation
ang = deg * Math.PI/180;
ctx.transform(1,0,0,1,100,100); // shift origin
ctx.transform(Math.cos(ang),Math.sin(ang),
-Math.sin(ang),Math.cos(ang), 0,0);
ctx.transform(1,0,0,1,50,0);
ctx.fillRect(0,0,30,10);
}
You can achieve the same this using the translate and rotate
for(deg = 0; deg < 360; deg+=20) {
ctx.setTransform(1,0,0,1,0,0); // reset transformation
ang = deg * Math.PI/180;
ctx.translate(100,100); // shift origin
ctx.rotate(ang);
ctx.translate(50,0);
ctx.fillRect(0,0,30,10);
}
I'm new to HTML5 Canvas and I'm trying to draw a triangle with rounded corners.
I have tried
ctx.lineJoin = "round";
ctx.lineWidth = 20;
but none of them are working.
Here's my code:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
var x = 18 / 2;
var y = 0;
var triangleWidth = 18;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
Could you help me?
Rounding corners
An invaluable function I use a lot is rounded polygon. It takes a set of 2D points that describe a polygon's vertices and adds arcs to round the corners.
The problem with rounding corners and keeping within the constraint of the polygons area is that you can not always fit a round corner that has a particular radius.
In these cases you can either ignore the corner and leave it as pointy or, you can reduce the rounding radius to fit the corner as best possible.
The following function will resize the corner rounding radius to fit the corner if the corner is too sharp and the lines from the corner not long enough to get the desired radius in.
Note the code has comments that refer to the Maths section below if you want to know what is going on.
roundedPoly(ctx, points, radius)
// ctx is the context to add the path to
// points is a array of points [{x :?, y: ?},...
// radius is the max rounding radius
// this creates a closed polygon.
// To draw you must call between
// ctx.beginPath();
// roundedPoly(ctx, points, radius);
// ctx.stroke();
// ctx.fill();
// as it only adds a path and does not render.
function roundedPoly(ctx, points, radiusAll) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut,radius;
// convert 2 points into vector form, polar form, and normalised
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
radius = radiusAll;
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
// for each point
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
//-----------------------------------------
// Part 1
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA < -1 ? -1 : sinA > 1 ? 1 : sinA);
//-----------------------------------------
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
if(p2.radius !== undefined){
radius = p2.radius;
}else{
radius = radiusAll;
}
//-----------------------------------------
// Part 2
halfAngle = angle / 2;
//-----------------------------------------
//-----------------------------------------
// Part 3
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
//-----------------------------------------
//-----------------------------------------
// Special part A
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
//-----------------------------------------
// Part 4
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
//-----------------------------------------
// Part 5
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
//-----------------------------------------
// Part 6
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
//-----------------------------------------
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
You may wish to add to each point a radius eg {x :10,y:10,radius:20} this will set the max radius for that point. A radius of zero will be no rounding.
The maths
The following illistration shows one of two possibilities, the angle to fit is less than 90deg, the other case (greater than 90) just has a few minor calculation differences (see code).
The corner is defined by the three points in red A, B, and C. The circle radius is r and we need to find the green points F the circle center and D and E which will define the start and end angles of the arc.
First we find the angle between the lines from B,A and B,C this is done by normalising the vectors for both lines and getting the cross product. (Commented as Part 1) We also find the angle of line BC to the line at 90deg to BA as this will help determine which side of the line to put the circle.
Now we have the angle between the lines, we know that half that angle defines the line that the center of the circle will sit F but we do not know how far that point is from B (Commented as Part 2)
There are two right triangles BDF and BEF which are identical. We have the angle at B and we know that the side DF and EF are equal to the radius of the circle r thus we can solve the triangle to get the distance to F from B
For convenience rather than calculate to F is solve for BD (Commented as Part 3) as I will move along the line BC by that distance (Commented as Part 4) then turn 90deg and move up to F (Commented as Part 5) This in the process gives the point D and moving along the line BA to E
We use points D and E and the circle center F (in their abstract form) to calculate the start and end angles of the arc. (done in the arc function part 6)
The rest of the code is concerned with the directions to move along and away from lines and which direction to sweep the arc.
The code section (special part A) uses the lengths of both lines BA and BC and compares them to the distance from BD if that distance is greater than half the line length we know the arc can not fit. I then solve the triangles to find the radius DF if the line BD is half the length of shortest line of BA and BC
Example use.
The snippet is a simple example of the above function in use. Click to add points to the canvas (needs a min of 3 points to create a polygon). You can drag points and see how the corner radius adapts to sharp corners or short lines. More info when snippet is running. To restart rerun the snippet. (there is a lot of extra code that can be ignored)
The corner radius is set to 30.
const ctx = canvas.getContext("2d");
const mouse = {
x: 0,
y: 0,
button: false,
drag: false,
dragStart: false,
dragEnd: false,
dragStartX: 0,
dragStartY: 0
}
function mouseEvents(e) {
mouse.x = e.pageX;
mouse.y = e.pageY;
const lb = mouse.button;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
if (lb !== mouse.button) {
if (mouse.button) {
mouse.drag = true;
mouse.dragStart = true;
mouse.dragStartX = mouse.x;
mouse.dragStartY = mouse.y;
} else {
mouse.drag = false;
mouse.dragEnd = true;
}
}
}
["down", "up", "move"].forEach(name => document.addEventListener("mouse" + name, mouseEvents));
const pointOnLine = {x:0,y:0};
function distFromLines(x,y,minDist){
var index = -1;
const v1 = {};
const v2 = {};
const v3 = {};
const point = P2(x,y);
eachOf(polygon,(p,i)=>{
const p1 = polygon[(i + 1) % polygon.length];
v1.x = p1.x - p.x;
v1.y = p1.y - p.y;
v2.x = point.x - p.x;
v2.y = point.y - p.y;
const u = (v2.x * v1.x + v2.y * v1.y)/(v1.y * v1.y + v1.x * v1.x);
if(u >= 0 && u <= 1){
v3.x = p.x + v1.x * u;
v3.y = p.y + v1.y * u;
dist = Math.hypot(v3.y - point.y, v3.x - point.x);
if(dist < minDist){
minDist = dist;
index = i;
pointOnLine.x = v3.x;
pointOnLine.y = v3.y;
}
}
})
return index;
}
function roundedPoly(ctx, points, radius) {
var i, x, y, len, p1, p2, p3, v1, v2, sinA, sinA90, radDirection, drawDirection, angle, halfAngle, cRadius, lenOut;
var asVec = function(p, pp, v) {
v.x = pp.x - p.x;
v.y = pp.y - p.y;
v.len = Math.sqrt(v.x * v.x + v.y * v.y);
v.nx = v.x / v.len;
v.ny = v.y / v.len;
v.ang = Math.atan2(v.ny, v.nx);
}
v1 = {};
v2 = {};
len = points.length;
p1 = points[len - 1];
for (i = 0; i < len; i++) {
p2 = points[(i) % len];
p3 = points[(i + 1) % len];
asVec(p2, p1, v1);
asVec(p2, p3, v2);
sinA = v1.nx * v2.ny - v1.ny * v2.nx;
sinA90 = v1.nx * v2.nx - v1.ny * -v2.ny;
angle = Math.asin(sinA); // warning you should guard by clampling
// to -1 to 1. See function roundedPoly in answer or
// Math.asin(Math.max(-1, Math.min(1, sinA)))
radDirection = 1;
drawDirection = false;
if (sinA90 < 0) {
if (angle < 0) {
angle = Math.PI + angle;
} else {
angle = Math.PI - angle;
radDirection = -1;
drawDirection = true;
}
} else {
if (angle > 0) {
radDirection = -1;
drawDirection = true;
}
}
halfAngle = angle / 2;
lenOut = Math.abs(Math.cos(halfAngle) * radius / Math.sin(halfAngle));
if (lenOut > Math.min(v1.len / 2, v2.len / 2)) {
lenOut = Math.min(v1.len / 2, v2.len / 2);
cRadius = Math.abs(lenOut * Math.sin(halfAngle) / Math.cos(halfAngle));
} else {
cRadius = radius;
}
x = p2.x + v2.nx * lenOut;
y = p2.y + v2.ny * lenOut;
x += -v2.ny * cRadius * radDirection;
y += v2.nx * cRadius * radDirection;
ctx.arc(x, y, cRadius, v1.ang + Math.PI / 2 * radDirection, v2.ang - Math.PI / 2 * radDirection, drawDirection);
p1 = p2;
p2 = p3;
}
ctx.closePath();
}
const eachOf = (array, callback) => { var i = 0; while (i < array.length && callback(array[i], i++) !== true); };
const P2 = (x = 0, y = 0) => ({x, y});
const polygon = [];
function findClosestPointIndex(x, y, minDist) {
var index = -1;
eachOf(polygon, (p, i) => {
const dist = Math.hypot(x - p.x, y - p.y);
if (dist < minDist) {
minDist = dist;
index = i;
}
});
return index;
}
// short cut vars
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var dragPoint;
var globalTime;
var closestIndex = -1;
var closestLineIndex = -1;
var cursor = "default";
const lineDist = 10;
const pointDist = 20;
var toolTip = "";
// main update function
function update(timer) {
globalTime = timer;
cursor = "crosshair";
toolTip = "";
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.globalAlpha = 1; // reset alpha
if (w !== innerWidth - 4 || h !== innerHeight - 4) {
cw = (w = canvas.width = innerWidth - 4) / 2;
ch = (h = canvas.height = innerHeight - 4) / 2;
} else {
ctx.clearRect(0, 0, w, h);
}
if (mouse.drag) {
if (mouse.dragStart) {
mouse.dragStart = false;
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex === -1){
polygon.push(dragPoint = P2(mouse.x, mouse.y));
}else{
polygon.splice(closestLineIndex+1,0,dragPoint = P2(mouse.x, mouse.y));
}
}else{
dragPoint = polygon[closestIndex];
}
}
dragPoint.x = mouse.x;
dragPoint.y = mouse.y
cursor = "none";
}else{
closestIndex = findClosestPointIndex(mouse.x,mouse.y, pointDist);
if(closestIndex === -1){
closestLineIndex = distFromLines(mouse.x,mouse.y,lineDist);
if(closestLineIndex > -1){
toolTip = "Click to cut line and/or drag to move.";
}
}else{
toolTip = "Click drag to move point.";
closestLineIndex = -1;
}
}
ctx.lineWidth = 4;
ctx.fillStyle = "#09F";
ctx.strokeStyle = "#000";
ctx.beginPath();
roundedPoly(ctx, polygon, 30);
ctx.stroke();
ctx.fill();
ctx.beginPath();
ctx.strokeStyle = "red";
ctx.lineWidth = 0.5;
eachOf(polygon, p => ctx.lineTo(p.x,p.y) );
ctx.closePath();
ctx.stroke();
ctx.strokeStyle = "orange";
ctx.lineWidth = 1;
eachOf(polygon, p => ctx.strokeRect(p.x-2,p.y-2,4,4) );
if(closestIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
dragPoint = polygon[closestIndex];
ctx.strokeRect(dragPoint.x-4,dragPoint.y-4,8,8);
cursor = "move";
}else if(closestLineIndex > -1){
ctx.strokeStyle = "red";
ctx.lineWidth = 4;
var p = polygon[closestLineIndex];
var p1 = polygon[(closestLineIndex + 1) % polygon.length];
ctx.beginPath();
ctx.lineTo(p.x,p.y);
ctx.lineTo(p1.x,p1.y);
ctx.stroke();
ctx.strokeRect(pointOnLine.x-4,pointOnLine.y-4,8,8);
cursor = "pointer";
}
if(toolTip === "" && polygon.length < 3){
toolTip = "Click to add a corners of a polygon.";
}
canvas.title = toolTip;
canvas.style.cursor = cursor;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
canvas {
border: 2px solid black;
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
I started by using #Blindman67 's answer, which works pretty well for basic static shapes.
I ran into the problem that when using the arc approach, having two points right next to each other is very different than having just one point. With two points next to each other, it won't be rounded, even if that is what your eye would expect. This is extra jarring if you are animating the polygon points.
I fixed this by using Bezier curves instead. IMO this is conceptually a little cleaner as well. I just make each corner with a quadratic curve where the control point is where the original corner was. This way, having two points in the same spot is virtually the same as only having one point.
I haven't compared performance but seems like canvas is pretty good at drawing Beziers.
As with #Blindman67 's answer, this doesn't actually draw anything so you will need to call ctx.beginPath() before and ctx.stroke() after.
/**
* Draws a polygon with rounded corners
* #param {CanvasRenderingContext2D} ctx The canvas context
* #param {Array} points A list of `{x, y}` points
* #radius {number} how much to round the corners
*/
function myRoundPolly(ctx, points, radius) {
const distance = (p1, p2) => Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2)
const lerp = (a, b, x) => a + (b - a) * x
const lerp2D = (p1, p2, t) => ({
x: lerp(p1.x, p2.x, t),
y: lerp(p1.y, p2.y, t)
})
const numPoints = points.length
let corners = []
for (let i = 0; i < numPoints; i++) {
let lastPoint = points[i]
let thisPoint = points[(i + 1) % numPoints]
let nextPoint = points[(i + 2) % numPoints]
let lastEdgeLength = distance(lastPoint, thisPoint)
let lastOffsetDistance = Math.min(lastEdgeLength / 2, radius)
let start = lerp2D(
thisPoint,
lastPoint,
lastOffsetDistance / lastEdgeLength
)
let nextEdgeLength = distance(nextPoint, thisPoint)
let nextOffsetDistance = Math.min(nextEdgeLength / 2, radius)
let end = lerp2D(
thisPoint,
nextPoint,
nextOffsetDistance / nextEdgeLength
)
corners.push([start, thisPoint, end])
}
ctx.moveTo(corners[0][0].x, corners[0][0].y)
for (let [start, ctrl, end] of corners) {
ctx.lineTo(start.x, start.y)
ctx.quadraticCurveTo(ctrl.x, ctrl.y, end.x, end.y)
}
ctx.closePath()
}
Styles for joining of lines such as ctx.lineJoin="round" apply to the stroke operation on paths - which is when their width, color, pattern, dash/dotted and similar line style attributes are taken into account.
Line styles do not apply to filling the interior of a path.
So to affect line styles a stroke operation is needed. In the following adaptation of posted code, I've translated canvas output to see the result without cropping, and stroked the triangle's path but not the rectangles below it:
var ctx = document.querySelector("canvas").getContext('2d');
ctx.scale(5, 5);
ctx.translate( 18, 12);
var x = 18 / 2;
var y = 0;
var triangleWidth = 48;
var triangleHeight = 8;
// how to round this triangle??
ctx.beginPath();
ctx.moveTo(x, y);
ctx.lineTo(x + triangleWidth / 2, y + triangleHeight);
ctx.lineTo(x - triangleWidth / 2, y + triangleHeight);
ctx.closePath();
ctx.fillStyle = "#009688";
ctx.fill();
// stroke the triangle path.
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.strokeStyle = "orange";
ctx.stroke();
ctx.fillStyle = "#8BC34A";
ctx.fillRect(0, triangleHeight, 9, 126);
ctx.fillStyle = "#CDDC39";
ctx.fillRect(9, triangleHeight, 9, 126);
<canvas width="800" height="600"></canvas>
i've a canvas dom element inside a div #content with transform rotateX(23deg) and #view with perspective 990px
<div id="view">
<div id="content">
<canvas></canvas>
</div>
</div>
if i draw a point (300,300) inside canvas, the projected coordinates are different (350, 250).
The real problem is when an object drawn in a canvas is interactive (click o drag and drop), the hit area is translated.
Which equation i've to use? Some kind of matrix?
Thanks for your support.
This is something I am dealing with now. Lets start out with something simple. Let's say your canvas is right up against the top left corner. If you click the mouse and make an arc on that spot it will be good.
canvasDOMObject.onmouseclick = (e) => {
const x = e.clientX;
const y = e.clientY;
}
If your canvas origin is not at client origin you would need to do something like this:
const rect = canvasDOMObject.getBoundingRect();
const x = e.clientX - rect.x;
const y = e.clientY - rect.y;
If you apply some pan, adding pan, when drawing stuff you need to un-pan it, pre-subtract the pan, when capturing the mouse point:
const panX = 30;
const panY = 40;
const rect = canvasDOMObject.getBoundingRect();
const x = e.clientX - rect.x - panX;
const y = e.clientY - rect.y - panY;
...
ctx.save();
ctx.translate(panX, panY);
ctx.beginPath();
ctx.strokeArc(x, y);
ctx.restore();
If you apply, for instance, a scale when you draw it, you would need to un-scale it when capturing the mouse point:
const panX = 30;
const panY = 40;
const scale = 1.5;
const rect = canvasDOMObject.getBoundingRect();
const x = (e.clientX - rect.x - panX) / scale;
const y = (e.clientY - rect.y - panY) / scale;
...
ctx.save();
ctx.translate(panX, panY);
ctx.scale(scale);
ctx.beginPath();
ctx.strokeArc(x, y);
ctx.restore();
The rotation I have not figured out yet but I'm getting there.
Alternative solution.
One way to solve the problem is to trace the ray from the mouse into the page and finding the point on the canvas where that ray intercepts.
You will need to transform the x and y axis of the canvas to match its transform. You will also have to project the ray from the desired point to the perspective point. (defined by x,y,z where z is perspective CSS value)
Note: I could not find much info about CSS perspective math and how it is implemented so it is just guess work from me.
There is a lot of math involved and i had to build a quick 3dpoint object to manage it all. I will warn you that it is not well designed (I dont have the time to inline it where needed) and will incur a heavy GC toll. You should rewrite the ray intercept and remove all the point clone calls and reuse points rather than create new ones each time you need them.
There are a few short cuts. The ray / face intercept assumes that the 3 points defining the face are the actual x and y axis but it does not check that this is so. If you have the wrong axis you will not get the correct pixel coordinate. Also the returned coordinate is relative to the point face.p1 (0,0) and is in the range 0-1 where 0 <= x <= 1 and 0 <= y <= 1 are points on the canvas.
Make sure the canvas resolution matches the display size. If not you will need to scale the axis and the results to fit.
DEMO
The demo project a set of points creating a cross through the center of the canvas. You will notice the radius of the projected circle will change depending on distance from the camera.
Note code is in ES6 and requires Babel to run on legacy browsers.
var divCont = document.createElement("div");
var canvas = document.createElement("canvas");
canvas.width = 400;
canvas.height = 400;
var w = canvas.width;
var h = canvas.height;
var cw = w / 2; // center
var ch = h / 2;
var ctx = canvas.getContext("2d");
// perspectiveOrigin
var px = cw; // canvas center
var py = 50; //
// perspective
var pd = 700;
var mat;
divCont.style.perspectiveOrigin = px + "px "+py+"px";
divCont.style.perspective = pd + "px";
divCont.style.transformStyle = "preserve-3d";
divCont.style.margin = "10px";
divCont.style.border = "1px black solid";
divCont.style.width = (canvas.width+8) + "px";
divCont.style.height = (canvas.height+8) + "px";
divCont.appendChild(canvas);
document.body.appendChild(divCont);
function getMatrix(){ // get canvas matrix
if(mat === undefined){
mat = new DOMMatrix().setMatrixValue(canvas.style.transform);
}else{
mat.setMatrixValue(canvas.style.transform);
}
}
function getPoint(x,y){ // get point on canvas
var ww = canvas.width;
var hh = canvas.height;
var face = createFace(
createPoint(mat.transformPoint(new DOMPoint(-ww / 2, -hh / 2))),
createPoint(mat.transformPoint(new DOMPoint(ww / 2, -hh / 2))),
createPoint(mat.transformPoint(new DOMPoint(-ww / 2, hh / 2)))
);
var ray = createRay(
createPoint(x - ww / 2, y - hh / 2, 0),
createPoint(px - ww / 2, py - hh / 2, pd)
);
return intersectCoord3DRayFace(ray, face);
}
// draw point projected onto the canvas
function drawPoint(x,y){
var p = getPoint(x,y);
if(p !== undefined){
p.x *= canvas.width;
p.y *= canvas.height;
ctx.beginPath();
ctx.arc(p.x,p.y,8,0,Math.PI * 2);
ctx.fill();
}
}
// main update function
function update(timer){
ctx.setTransform(1,0,0,1,0,0); // reset transform
ctx.globalAlpha = 1; // reset alpha
ctx.fillStyle = "green";
ctx.fillRect(0,0,w,h);
ctx.lineWidth = 10;
ctx.strokeRect(0,0,w,h);
canvas.style.transform = "rotateX("+timer/100+"deg)" + " rotateY("+timer/50+"deg)";
getMatrix();
ctx.fillStyle = "gold";
drawPoint(cw,ch);
for(var i = -200; i <= 200; i += 40){
drawPoint(cw + i,ch);
drawPoint(cw ,ch + i);
}
requestAnimationFrame(update);
}
requestAnimationFrame(update);
// Math functions to find x,y pos on plain.
// Warning this code is not built for SPEED and will incure a lot of GC hits
const small = 1e-6;
var pointFunctions = {
add(p){
this.x += p.x;
this.y += p.y;
this.z += p.z;
return this;
},
sub(p){
this.x -= p.x;
this.y -= p.y;
this.z -= p.z;
return this;
},
mul(mag){
this.x *= mag;
this.y *= mag;
this.z *= mag;
return this;
},
mag(){ // get length
return Math.hypot(this.x,this.y,this.z);
},
cross(p){
var p1 = this.clone();
p1.x = this.y * p.z - this.z * p.y;
p1.y = this.z * p.x - this.x * p.z;
p1.z = this.x * p.y - this.y * p.x;
return p1;
},
dot(p){
return this.x * p.x + this.y * p.y + this.z * p.z;
},
isZero(){
return Math.abs(this.x) < small && Math.abs(this.y) < small && Math.abs(this.z) < small;
},
clone(){
return Object.assign({
x : this.x,
y : this.y,
z : this.z,
},pointFunctions);
}
}
function createPoint(x,y,z){
if(y === undefined){ // quick add overloaded for DOMPoint
y = x.y;
z = x.z;
x = x.x;
}
return Object.assign({
x, y, z,
}, pointFunctions);
}
function createRay(p1, p2){
return { p1, p2 };
}
function createFace(p1, p2, p3){
return { p1,p2, p3 };
}
// Returns the x,y coord of ray intercepting face
// ray is defined by two 3D points and is infinite in length
// face is 3 points on the intereceptin plane
// For correct intercept point face p1-p2 should be at 90deg to p1-p3 (x, and y Axis)
// returns unit coordinates x,y on the face with the origin at face.p1
// If there is no solution then returns undefined
function intersectCoord3DRayFace(ray, face ){
var u = face.p2.clone().sub(face.p1);
var v = face.p3.clone().sub(face.p1);
var n = u.cross(v);
if(n.isZero()){
return; // return undefined
}
var vr = ray.p2.clone().sub(ray.p1);
var b = n.dot(vr);
if (Math.abs(b) < small) { // ray is parallel face
return; // no intercept return undefined
}
var w = ray.p1.clone().sub(face.p1);
var a = -n.dot(w);
var uDist = a / b;
var intercept = ray.p1.clone().add(vr.mul(uDist)); // intersect point
var uu = u.dot(u);
var uv = u.dot(v);
var vv = v.dot(v);
var dot = uv * uv - uu * vv;
w = intercept.clone().sub(face.p1);
var wu = w.dot(u);
var wv = w.dot(v);
var x = (uv * wv - vv * wu) / dot;
var y = (uv * wu - uu * wv) / dot;
return {x,y};
}
I have a square image like this:
I am trying to stretch this image into a polygon like this:
So far I have been able to create a polygon on the canvas as the above image using the following javascript:
function drawCanvas() {
var c2 = document.getElementById('myCanvas6').getContext('2d');
var img = document.getElementById("scream");
c2.fillStyle = '#000';
c2.beginPath();
c2.moveTo(20, 20);
c2.lineTo(320, 50);
c2.lineTo(320, 170);
c2.lineTo(20, 200);
//c2.drawImage(img, 150, 10, img.width, img.height);
c2.closePath();
c2.fill();
}
I tried using drawImage() method, but it does not stretch the points A, B, C, D to the new positions. Is there anyway this can be achieved?
The 2D canvas is called 2D for a very good reason. You can not transform a square such that any of its side converge (are not parallel) hence 2D
But where there is a need there is always a way..
You can do it by cutting the image into slices and then draw each slice slightly smaller than the last.
We humans don't like to see an image distort when it converges, so you need to add the distortion we expect, perspective. The further away the object the smaller the distance between points appears to the eye.
So the function below draws an image with the top and bottom edges converging..
It is not true 3D but it does make the image appear as distorted as jus converging the top and bottom without decreasing the y step. The animation introduced a bit of an optical illusion. the second render shortens the image to make it appear a little less fake.
See the code on how to use the function.
/** CreateImage.js begin **/
// creates a blank image with 2d context
var createImage=function(w,h){var i=document.createElement("canvas");i.width=w;i.height=h;i.ctx=i.getContext("2d");return i;}
/** CreateImage.js end **/
var can = createImage(512,512);
document.body.appendChild(can);
var ctx = can.ctx;
const textToDisplay = "Perspective"
const textSize = 80;
ctx.font = textSize+"px arial";
var w = ctx.measureText(textToDisplay).width + 8;
var text = createImage(w + 64,textSize + 32);
text.ctx.fillStyle = "#08F";
text.ctx.strokeStyle = "black";
text.ctx.lineWidth = 16;
text.ctx.fillRect(0,0,text.width,text.height);
text.ctx.strokeRect(0,0,text.width,text.height);
text.ctx.font = textSize+"px arial";
text.ctx.fillStyle = "#F80";
text.ctx.strokeStyle = "Black";
text.ctx.lineWidth = 4;
text.ctx.strokeText(textToDisplay,38,textSize + 8);
text.ctx.fillText(textToDisplay,38,textSize + 8);
// Not quite 3D
// ctx is the context to draw to
// image is the image to draw
// x1,x2 left and right edges of the image
// zz1,zz2 top offset for left and right
// image top edge has a slops from zz1 to zz2
// yy if the position to draw top. This is where the top would be if z = 0
function drawPerspective(ctx, image, x1, zz1, x2, zz2, yy){
var x, w, h, h2,slop, topLeft, botLeft, zDistR, zDistL, lines, ty;
w = image.width; // image size
h = image.height;
h2 = h /2; // half height
slop = (zz2 - zz1) / (x2 - x1); // Slope of top edge
z1 = h2 - zz1; // Distance (z) to first line
z2 = (z1 / (h2 - zz2)) * z1 - z1; // distance (z) between first and last line
if(z2 === 0){ // if no differance in z then is square to camera
topLeft = - x1 * slop + zz1; // get scan line top left edge
ctx.drawImage(image,0, 0, w, h,x1, topLeft + yy ,x2-x1, h - topLeft * 2) // render to desination
return;
}
// render each display line getting all pixels that will be on that line
for (x = x1; x < x2; x++) { // for each line horizontal line
topLeft = (x - x1) * slop + zz1; // get scan line top left edge
botLeft = ((x + 1) - x1) * slop + zz1; // get scan line bottom left edge
zDistL = (z1 / (h2 - topLeft)) * z1; // get Z distance to Left of this line
zDistR = (z1 / (h2 - botLeft)) * z1; // get Z distance to right of this line
ty = ((zDistL - z1) / z2) * w; // get y bitmap coord
lines = ((zDistR - z1) / z2) * w - ty;// get number of lines to copy
ctx.drawImage(image,
ty % w, 0, lines, h, // get the source location of pixel
x, topLeft + yy,1 , h - topLeft * 2 // render to desination
);
}
}
var animTick = 0;
var animRate = 0.01;
var pos = 0;
var short = 0;
function update1(){
animTick += animRate;
pos = Math.sin(animTick) * 20 + 20;
short = Math.cos((pos / 40) * Math.PI) * text.width * 0.12 - text.width * 0.12;
ctx.clearRect(0,0,can.width,can.height)
drawPerspective(ctx,text,0,0,text.width,pos,20)
drawPerspective(ctx,text,0,0,text.width+short,pos,textSize + 32 + 30)
requestAnimationFrame(update1);
}
update1();
I think this is a good solution for you: http://jsfiddle.net/fQk4h/
Here is the magic:
for (i = 0; i < w; i++) {
dy = (leftTop * (w - i)) / w;
dh = (leftBot * (w - i) + h * i) / w;
ctx.drawImage(tmpCtx.canvas,
i, 0, 1, h,
i, dy, 1, dh);
}
ctx.restore();