Javascript - Rotating a rectangle before writing to canvas - javascript

I've been having an enormous amount of difficulty getting a set of rectangles to face a certain point on a canvas block. I'm iterating through a set of points, and drawing rectangles, which are supposed to all face a certain point.
First I set up the Radius, Focus point and Rectangle
var Angle = Math.PI;
_.each(Points, function(Point) {
var Radius = 10;
var Focus = {
X: 500,
Y: 1000
}
var Rect = [
{
X: (Point.X - Radius/2),
Y: (Point.Y - Radius/2)
},
{
X: (Point.X + Radius/2),
Y: (Point.Y - Radius/2)
},
{
X: (Point.X + Radius/2),
Y: (Point.Y + Radius/2)
},
{
X: (Point.X - Radius/2),
Y: (Point.Y + Radius/2)
}
];
Then, using underscore, I iterate through and used the method I read about on another response.
var index = 0;
_.each(Rect, function (V) {
var dx2 = Focus.X - V.X;
var dy2 = Focus.Y - V.Y;
var dx2_ = dx2 * Math.cos(Angle) - dy2 * Math.sin(Angle);
var dy2_ = dy2 * Math.sin(Angle) + dy2 * Math.cos(Angle);
V.X = dx2_ + V.X;
V.Y = dy2_ + V.Y;
Rect[index] = V;
index++;
});
Then its all set up in the normal canvas stuff. The context and the fillStyle are defined beforehand.
ctx.beginPath();
ctx.moveTo(Rect[0].X, Rect[0].Y);
ctx.lineTo(Rect[1].X, Rect[1].Y);
ctx.lineTo(Rect[2].X, Rect[2].Y);
ctx.lineTo(Rect[3].X, Rect[3].Y);
ctx.lineTo(Rect[0].X, Rect[0].Y);
ctx.fill();
});
These are the initial squares (without the rotation each loop)
Without rotation
This is what I get instead (seems like the rectangles are being rotated around the point instead of rotating towards the point)
With angle set to pi
The behavior that I'm trying to get is to have them facing towards the bottom middle. (500,1000) if the canvas is 1000x1000.
JSFiddle: http://jsfiddle.net/nmartin413/cMwTn/7/
I can't for the life of me figure out whats going wrong. If there's anything that stands out to you, please let me know :). Sorry for the long question, but this has really frustrated me.
Thanks in advance!

Try not to draw the rectangles yourself, use ctx.rotate(angle); and ctx.fillRect(-Radius/2, -Radius/2, width, height); as it is more simple, and code-wise - much easier to read and manage. To accomplish this rotation, save and redraw the canvas every time you apply a transformation to a rectangle, using ctx.save(); and ctx.restore();
JS:
$(document).ready(function() {
var Canvas = $('canvas')[0];
var Focus = {
X: 500,
Y: 1000
};
var f = Focus;
var angle = 0;
var Radius = 10;
_.each(Points, function($Point) {
var ctx = Canvas.getContext('2d');
var x = $Point.X;
var y = $Point.Y;
var width = Radius;
var height = Radius;
var cx = x - 0.5 * width;
var cy = y - 0.5 * height;
ctx.save();
ctx.translate(x,y);
var angle = (Math.atan2(f.Y-y,f.X-x));
ctx.rotate(angle);
ctx.fillStyle = "#ED1B24";
ctx.fillRect(-Radius/2, -Radius/2, width, height);
ctx.restore();
});
});
JSFiddle: http://jsfiddle.net/mJNas/

Related

js canvas simulate infinite, pan- and zoomable grid

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>

How to draw herringbone pattern on html canvas

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>

Canvas get perspective point

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};
}

Raphael.js has weird behavior on drag

I have circle with a line from the center to the edge of the circle. The user can click on and drag the line and get the degrees from wherever they set the line. I'm using the code from the answer here and have adjusted the code accordingly.
Everything works fine, but the farther down the page the element affected by raphael is, the farther outside of the circle the mouse has to be in order to drag the line. jsfiddle
var canvas = Raphael('pivot', 0, 0, 320, 320);
var clock = canvas.circle(200, 150, 100).attr("stroke-width", 2);
canvas.circle(200, 150, 3).attr("fill", "#000");
var angleplus = 360,
rad = Math.PI / 180,
cx = 200,
cy = 150,
r = 90,
startangle = -90,
angle = 90,
x, y, endangle;
for (i = 1; i < 5; i++) {
endangle = startangle + angle;
x = cx + r * Math.sin(endangle * rad);
y = cy - r * Math.cos(endangle * rad);
canvas.text(x, y, endangle);
startangle = endangle;
}
var hand = canvas.path("M200 50L200 150").attr("stroke-width", 5);
hand.drag( move, start, end );
function move (dx,dy,x,y) {
var pt = this.node.ownerSVGElement.createSVGPoint();
pt.x = x;
pt.y = y;
var angle = ( 90 + Math.atan2(pt.y - clock.attr('cy') - 5, pt.x - clock.attr('cx') - 5 ) * 180 / Math.PI + 360) % 360;
this.rotate(angle, 200,150);
this.angle = angle;
}
function start() {
};
function end() {
alert(parseInt(this.angle));
}
I'm wondering why this happens and if it's even fixable?
Here is an updated original one, a bit added and another bit of redundancy removed, and I've added an extra fiddle to the original answer to reflect it.
The key bits I've added are...
var cpt = clock.node.ownerSVGElement.createSVGPoint();
cpt.x = clock.attr('cx');
cpt.y = clock.attr('cy');
cpt = cpt.matrixTransform(clock.node.getScreenCTM());
The centre of the clock has now moved, so using cx of the circle alone, doesn't really make sense relative to the mouse event. We need to take into account any offset and transforms on the screen to the clock.
We can get this with getScreenCTM (get the matrix from element to the screen), and we can use that matrix with the original cx, cy to figure out where it is on the screen.
Then later, instead of cx, cy, we use the new adjusted coordinates cpt.x/y
var angle = ( 90 + Math.atan2(y - cpt.y - 5, x - cpt.x - 5 ) * 180 / Math.PI + 360)
jsfiddle - drag any hand

Drawing a spiral on an HTML canvas using JavaScript

I have searched and haven't found anything really on how to draw spirals in canvas using JavaScript.
I thought it might be possible to do it with the bezier curve and if that didn't work use lineTo(), but that seemed a lot harder.
Also, to do that I'm guessing I would have to use trigonometry and graphing with polar coordinates and its been a while since I did that. If that is the case could you point me in the right direction on the math.
The Archimedean spiral is expressed as r=a+b(angle). Convert that into x, y coordinate, it will be expressed as x=(a+b*angle)*cos(angle), y=(a+b*angle)*sin(angle). Then you can put angle in a for loop and do something like this:
for (i=0; i< 720; i++) {
angle = 0.1 * i;
x=(1+angle)*Math.cos(angle);
y=(1+angle)*Math.sin(angle);
context.lineTo(x, y);
}
Note the above assumes a = 1 and b = 1.
Here is a jsfiddle link: http://jsfiddle.net/jingshaochen/xJc7M/
This is a slightly-changed, javascript-ified version of a Java spiral I once borrowed from here
It uses lineTo() and its not all that hard.
<!DOCTYPE HTML>
<html><body>
<canvas id="myCanvas" width="300" height="300" style="border:1px solid #c3c3c3;"></canvas>
<script type="text/javascript">
var c=document.getElementById("myCanvas");
var cxt=c.getContext("2d");
var centerX = 150;
var centerY = 150;
cxt.moveTo(centerX, centerY);
var STEPS_PER_ROTATION = 60;
var increment = 2*Math.PI/STEPS_PER_ROTATION;
var theta = increment;
while( theta < 40*Math.PI) {
var newX = centerX + theta * Math.cos(theta);
var newY = centerY + theta * Math.sin(theta);
cxt.lineTo(newX, newY);
theta = theta + increment;
}
cxt.stroke();
</script></body></html>
Here's a function I wrote for drawing Archimedean spirals:
CanvasRenderingContext2D.prototype.drawArchimedeanSpiral =
CanvasRenderingContext2D.prototype.drawArchimedeanSpiral ||
function(centerX, centerY, stepCount, loopCount,
innerDistance, loopSpacing, rotation)
{
this.beginPath();
var stepSize = 2 * Math.PI / stepCount,
endAngle = 2 * Math.PI * loopCount,
finished = false;
for (var angle = 0; !finished; angle += stepSize) {
// Ensure that the spiral finishes at the correct place,
// avoiding any drift introduced by cumulative errors from
// repeatedly adding floating point numbers.
if (angle > endAngle) {
angle = endAngle;
finished = true;
}
var scalar = innerDistance + loopSpacing * angle,
rotatedAngle = angle + rotation,
x = centerX + scalar * Math.cos(rotatedAngle),
y = centerY + scalar * Math.sin(rotatedAngle);
this.lineTo(x, y);
}
this.stroke();
}
there is a fine free tool that will help if you have illustrator
ai2canvas
it will create all the curves to javascript in html canvas tag for you!
(if you are looking for archmedes spiral than you will first have to get it from coreldraw and copy that to illustrator, because the default spiral tool enlarges the angle with each point)
this is example of drawing spiral using function below:
spiral(ctx, {
start: {//starting point of spiral
x: 200,
y: 200
},
angle: 30 * (Math.PI / 180), //angle from starting point
direction: false,
radius: 100, //radius from starting point in direction of angle
number: 3 // number of circles
});
spiral drawing code:
spiral = function(ctx,obj) {
var center, eAngle, increment, newX, newY, progress, sAngle, tempTheta, theta;
sAngle = Math.PI + obj.angle;
eAngle = sAngle + Math.PI * 2 * obj.number;
center = {
x: obj.start.x + Math.cos(obj.angle) * obj.radius,
y: obj.start.y + Math.sin(obj.angle) * obj.radius
};
increment = 2 * Math.PI / 60/*steps per rotation*/;
theta = sAngle;
ctx.beginPath();
ctx.moveTo(center.x, center.y);
while (theta <= eAngle + increment) {
progress = (theta - sAngle) / (eAngle - sAngle);
tempTheta = obj.direction ? theta : -1 * (theta - 2 * obj.angle);
newX = obj.radius * Math.cos(tempTheta) * progress;
newY = obj.radius * Math.sin(tempTheta) * progress;
theta += increment;
ctx.lineTo(center.x + newX, center.y + newY);
}
ctx.stroke();
};
The following code approximates a spiral as a collection of quarters of a circle each with a slightly larger radius. It might look worse than an Archimedes spiral for small turning numbers but it should run faster.
function drawSpiral(ctx, centerx, centery, innerRadius, outerRadius, turns=2, startAngle=0){
ctx.save();
ctx.translate(centerx, centery);
ctx.rotate(startAngle);
let r = innerRadius;
let turns_ = Math.floor(turns*4)/4;
let dr = (outerRadius - innerRadius)/turns_/4;
let cx = 0, cy = 0;
let directionx = 0, directiony = -1;
ctx.beginPath();
let angle=0;
for(; angle < turns_*2*Math.PI; angle += Math.PI/2){
//draw a quarter arc around the center point (x, cy)
ctx.arc( cx, cy, r, angle, angle + Math.PI/2);
//move the center point and increase the radius so we can draw a bigger arc
cx += directionx*dr;
cy += directiony*dr;
r+= dr;
//rotate direction vector by 90 degrees
[directionx, directiony] = [ - directiony, directionx ];
}
//draw the remainder of the last quarter turn
ctx.arc( cx, cy, r, angle, angle + 2*Math.PI*( turns - turns_ ))
ctx.stroke();
ctx.restore();
}
Result:

Categories

Resources