Determine if mouse was clicked on a rotated rectangle on a canvas - javascript

I am using the example that has been posted in the following link:
https://riptutorial.com/html5-canvas/example/19666/a-transformation-matrix-to-track-translated--rotated---scaled-shape-s-
I am trying to use this example, with some modifications to a allow for rotating 2 rectangles on the canvas. There are several issues (and they may be related), but let me just start with one:
Mouse click no longer works.
I hope someone can help point to me to what is the needed remedy as I struggled with this for a over a day and obviously I am not savvy with Objects, Classes in Javascrip. The code is here: https://jsfiddle.net/jackmstein/ngyfrcms/2/
<!doctype html>
<html>
<head>
<style>
body{ background-color:white; }
#canvas{border:1px solid red; }
</style>
<script>
window.onload=(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
// Transformation Matrix "Class"
var TransformationMatrix=( function(){
// private
var self;
var m=[1,0,0,1,0,0];
var reset=function(){ var m=[1,0,0,1,0,0]; }
var multiply=function(mat){
var m0=m[0]*mat[0]+m[2]*mat[1];
var m1=m[1]*mat[0]+m[3]*mat[1];
var m2=m[0]*mat[2]+m[2]*mat[3];
var m3=m[1]*mat[2]+m[3]*mat[3];
var m4=m[0]*mat[4]+m[2]*mat[5]+m[4];
var m5=m[1]*mat[4]+m[3]*mat[5]+m[5];
m=[m0,m1,m2,m3,m4,m5];
}
var screenPoint=function(transformedX,transformedY){
// invert
var d =1/(m[0]*m[3]-m[1]*m[2]);
im=[ m[3]*d, -m[1]*d, -m[2]*d, m[0]*d, d*(m[2]*m[5]-m[3]*m[4]), d*(m[1]*m[4]-m[0]*m[5]) ];
// point
return({
x:transformedX*im[0]+transformedY*im[2]+im[4],
y:transformedX*im[1]+transformedY*im[3]+im[5]
});
}
var transformedPoint=function(screenX,screenY){
return({
x:screenX*m[0] + screenY*m[2] + m[4],
y:screenX*m[1] + screenY*m[3] + m[5]
});
}
// public
function TransformationMatrix(){
self=this;
}
// shared methods
TransformationMatrix.prototype.translate=function(x,y){
var mat=[ 1, 0, 0, 1, x, y ];
multiply(mat);
};
TransformationMatrix.prototype.rotate=function(rAngle){
var c = Math.cos(rAngle);
var s = Math.sin(rAngle);
var mat=[ c, s, -s, c, 0, 0 ];
multiply(mat);
};
TransformationMatrix.prototype.scale=function(x,y){
var mat=[ x, 0, 0, y, 0, 0 ];
multiply(mat);
};
TransformationMatrix.prototype.skew=function(radianX,radianY){
var mat=[ 1, Math.tan(radianY), Math.tan(radianX), 1, 0, 0 ];
multiply(mat);
};
TransformationMatrix.prototype.reset=function(){
reset();
}
TransformationMatrix.prototype.setContextTransform=function(ctx){
ctx.setTransform(m[0],m[1],m[2],m[3],m[4],m[5]);
}
TransformationMatrix.prototype.resetContextTransform=function(ctx){
ctx.setTransform(1,0,0,1,0,0);
}
TransformationMatrix.prototype.getTransformedPoint=function(screenX,screenY){
return(transformedPoint(screenX,screenY));
}
TransformationMatrix.prototype.getScreenPoint=function(transformedX,transformedY){
return(screenPoint(transformedX,transformedY));
}
TransformationMatrix.prototype.getMatrix=function(){
var clone=[m[0],m[1],m[2],m[3],m[4],m[5]];
return(clone);
}
// return public
return(TransformationMatrix);
})();
// DEMO starts here
// create a rect and add a transformation matrix
// to track it's translations, rotations & scalings
var rect1={x:30,y:30,w:50,h:35,matrix:new TransformationMatrix()};
// draw the untransformed rect in black
// ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
// Demo: label
// ctx.font='11px arial';
// ctx.fillText('Untransformed Rect',rect.x,rect.y-10);
// transform the canvas & draw the transformed rect in red
ctx.translate(100,0);
ctx.scale(2,2);
ctx.rotate(Math.PI/12);
// draw the transformed rect
ctx.strokeStyle='red';
ctx.strokeRect(rect1.x, rect1.y, rect1.w, rect1.h);
ctx.font='6px arial';
// Demo: label
ctx.fillText('Rect: Translated, rotated & scaled',rect1.x,rect1.y-6);
// reset the context to untransformed state
ctx.setTransform(1,0,0,1,0,0);
// record the transformations in the matrix
var m=rect1.matrix;
m.translate(100,0);
m.scale(2,2);
m.rotate(Math.PI/12);
// use the rect's saved transformation matrix to reposition,
// resize & redraw the rect
ctx.strokeStyle='blue';
drawTransformedRect(rect1);
// Demo: instructions
ctx.font='14px arial';
ctx.fillText('Demo: click inside the blue rect',30,200);
// DEMO starts here
// create a rect and add a transformation matrix
// to track it's translations, rotations & scalings
var rect2={x:150,y:30,w:50,h:35,matrix:new TransformationMatrix()};
// draw the untransformed rect in black
// ctx.strokeRect(rect.x, rect.y, rect.w, rect.h);
// Demo: label
// ctx.font='11px arial';
// ctx.fillText('Untransformed Rect',rect.x,rect.y-10);
// transform the canvas & draw the transformed rect in red
ctx.translate(100,0);
ctx.scale(2,2);
ctx.rotate(Math.PI/12);
// draw the transformed rect
ctx.strokeStyle='red';
ctx.strokeRect(rect2.x, rect2.y, rect2.w, rect2.h);
ctx.font='6px arial';
// Demo: label
ctx.fillText('Rect: Translated, rotated & scaled',rect2.x,rect2.y-6);
// reset the context to untransformed state
ctx.setTransform(1,0,0,1,0,0);
// record the transformations in the matrix
var m=rect2.matrix;
m.translate(100,0);
m.scale(2,2);
m.rotate(Math.PI/12);
// use the rect's saved transformation matrix to reposition,
// resize & redraw the rect
ctx.strokeStyle='blue';
drawTransformedRect(rect2);
// Demo: instructions
// redraw a rect based on it's saved transformation matrix
function drawTransformedRect(r){
// set the context transformation matrix using the rect's saved matrix
m.setContextTransform(ctx);
// draw the rect (no position or size changes needed!)
ctx.rect( r.x, r.y, r.w, r.h );
// reset the context transformation to default (==untransformed);
m.resetContextTransform(ctx);
}
// is the point in the transformed rectangle?
function isPointInTransformedRect(r,transformedX,transformedY){
var p=r.matrix.getScreenPoint(transformedX,transformedY);
var x=p.x;
var y=p.y;
return(x>r.x && x<r.x+r.w && y>r.y && y<r.y+r.h);
}
// listen for mousedown events
canvas.onmousedown=handleMouseDown;
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// is the mouse inside the transformed rect?
var rect1={x:30,y:30,w:50,h:35,matrix:new TransformationMatrix()};
if(isPointInTransformedRect(rect1,mouseX,mouseY)){
alert('You clicked in the transformed Rect 1');
}
var rect2={x:150,y:30,w:50,h:35,matrix:new TransformationMatrix()};
if(isPointInTransformedRect(rect2,mouseX,mouseY)){
alert('You clicked in the transformed Rect 2');
}
}
// Demo: redraw transformed rect without using
// context transformation commands
function drawTransformedRect(r,color){
var m=r.matrix;
var tl=m.getTransformedPoint(r.x,r.y);
var tr=m.getTransformedPoint(r.x+r.w,r.y);
var br=m.getTransformedPoint(r.x+r.w,r.y+r.h);
var bl=m.getTransformedPoint(r.x,r.y+r.h);
ctx.beginPath();
ctx.moveTo(tl.x,tl.y);
ctx.lineTo(tr.x,tr.y);
ctx.lineTo(br.x,br.y);
ctx.lineTo(bl.x,bl.y);
ctx.closePath();
ctx.strokeStyle=color;
ctx.stroke();
}
}); // end window.onload
</script>
</head>
<body>
<canvas id="canvas" width=600 height=250></canvas>
</body>
</html>

Point over rotated rectangle
NOTE this answer uses a uniform transform. (Skew will not work, and both x and y axis must have the same scale);
To keep it all as simple as possible the rectangle (box) is defined by its center pos and its width and height size.
const Point = (x = 0, y = 0) => ({x, y});
const Size = (w = 0, h = 0) => ({w, h});
const Rect = (pos, size) = ({pos, size});
We can create the rotated (transformed) rect by creating the transform as needed. (quicker than creating a DOMMatrix or custom matrix)
function pathRect(rect, ang, scale) { // ang in radians
const xax = Math.cos(ang) * scale;
const xay = Math.sin(ang) * scale;
ctx.setTransform(xax, xay, -xay, xax, rect.pos.x, rect.pos.y);
ctx.rect(-rect.size.w * 0.5, -rect.size.h * 0.5, rect.size.w, rect.size.h);
}
To find the point relative to the rect we apply the inverse (reverse) of the transform applied to the rect.
relPoint is converted to a box relative unit value. That is, if the point is at the top left (of rotated rect) relPoint will be {x:0, y:0}, at center {x: 0.5, y: 0.5} and bottom right is {x: 1, y: 1}. This simplifies the bounds test (see demo)
function pointRelative2Rect(point, relPoint, rect, ang, scale) {
const xax = Math.cos(-ang) / scale;
const xay = Math.sin(-ang) / scale;
const x = point.x - rect.pos.x;
const y = point.y - rect.pos.y;
relPoint.x = (xax * x - xay * y) / rect.size.w + 0.5;
relPoint.y = (xay * x + xax * y) / rect.size.h + 0.5;
}
Thus if we have a mouse pos then we can find when its over the rect using
pointRelative2Rect(mouse, mouseRel, rect, rot, scale);
if (mouseRel.x < 0 || mouseRel.x > 1 || mouseRel.y < 0 || mouseRel.y > 1) {
// mouse outside rect
} else {
// mouse over rect
}
Demo
Demo creates a random rectangle that is animated to rotate and scale over time.
It uses the methods outlined above to test if the mouse is inside the rect.
The box will change to red when mouse is over.
const ctx = canvas.getContext("2d");
const w = 256, h = 256;
const Point = (x = 0, y = 0) => ({x, y});
const Size = (w = 0, h = 0) => ({w, h});
const Rect = (pos, size) => ({pos, size});
const randI = (min, max) => Math.random() * (max - min) + min;
const mouse = Point(), mouseRel = Point();
document.addEventListener("mousemove", e => { mouse.x = e.pageX; mouse.y = e.pageY; });
const randRect = () => Rect(
Point(randI(40, 210), randI(40, 210)),
Size(randI(10, 30), randI(10, 30))
);
const rect = randRect();
function pathRect(rect, ang, scale) { // ang in radians
const xax = Math.cos(ang) * scale;
const xay = Math.sin(ang) * scale;
ctx.setTransform(xax, xay, -xay, xax, rect.pos.x, rect.pos.y);
ctx.rect(-rect.size.w * 0.5, -rect.size.h * 0.5, rect.size.w, rect.size.h);
}
function pointRelative2Rect(point, resPoint, rect, ang, scale) {
const xax = Math.cos(-ang) / scale;
const xay = Math.sin(-ang) / scale;
const x = point.x - rect.pos.x;
const y = point.y - rect.pos.y;
resPoint.x = (xax * x - xay * y) / rect.size.w + 0.5;
resPoint.y = (xay * x + xax * y) / rect.size.h + 0.5;
}
requestAnimationFrame(renderLoop);
function renderLoop(time) {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0, 0, w, h);
const rot = time / 3000;
const scale = Math.sin(time / 2777) * 0.5 + 2.1;
ctx.beginPath();
pointRelative2Rect(mouse, mouseRel, rect, rot, scale);
if (mouseRel.x < 0 || mouseRel.x > 1 || mouseRel.y < 0 || mouseRel.y > 1) {
canvas.style.cursor = "default";
ctx.strokeStyle = "#000";
} else {
canvas.style.cursor = "pointer";
ctx.strokeStyle = "#F00";
}
pathRect(rect, rot, scale);
ctx.lineWidth = 1 / scale;
ctx.stroke();
requestAnimationFrame(renderLoop);
}
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id="canvas" width="256" height="256"><canvas>

Related

how to draw lines simultaneously show x & y coordinators on top of mouse pointer using canvas and javascript

draw lines while mouseDown and simultaneously show x & y coordinators on top of mouse pointer while MouseMove using canvas and javascript.
Here x & y coordinators are continuously drawing on top of mouse pointer while MouseMove. Then I am unable Draw Lines while MouseDown since i am using ctxTemp.clearRect(0,0,canvasTemp.width,canvasTemp.height);
if i am not using ctxTemp.clearRect(0,0,canvasTemp.width,canvasTemp.height); then x & y coordinators are continuously drawing on top of mouse pointer while MouseMove.
thanks is advance.
Double buffer
It is a common task to render addition guides (coordinates, widgets, etc...) on the canvas while creating content.
Using a single canvas this become problematic as you are overwriting the content by clearing, or the just painting the guides .
The solution is to use an additional (or more) canvas to separate the content from the guides.
Example
The example shows how this is done.
A second canvas is created call drawing. It matches the size of the canvas on the page.
The mouse draws the stroke to the second canvas.
The main update function draws the second canvas onto the main canvas and then draws the mouse position in a box over that.
As you can not draw outside the canvas some additional code is needed when drawing the mouse position to prevent it from going outside the canvas.
requestAnimationFrame(update);
const ctx = canvas.getContext("2d");
var w = canvas.width, h = canvas.height;
const drawing = createImage(w, h); // create canvas to hold drawing
const pointQueue = []; // holds points when mouse button down
drawing.ctx.lineWidth = 4;
drawing.ctx.strokeStyle = "#F00";
drawing.ctx.lineJoin = "round";
drawing.ctx.lineCap = "round";
ctx.font = "16px Arial";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
/* add mouse listeners */
const bounds = canvas.getBoundingClientRect();
const mouse = {x: 0, y: 0, button: false}, events = ["down", "up", "move"];
events.forEach(name => document.addEventListener("mouse" + name, mouseEvents));
function drawMousePos(ctx) {
const text = "X: " + mouse.x.toFixed(0) + " Y: " + mouse.y.toFixed(0);
const width = ctx.measureText(text).width + 8;
var x = mouse.x, y = mouse.y - 18;
if (x + width / 2 > w) { x = w - width / 2 }
if (x - width / 2 < 0) { x = width / 2 }
if (y - 10 < 0) { y = 10 }
if (y + 10 > h) { y = h - 10 }
ctx.fillStyle = "#EEC8";
ctx.fillRect(x - width / 2, y - 12, width , 20);
ctx.strokeRect(x - width / 2, y - 12, width, 20);
ctx.fillStyle = "#000C";
ctx.fillText(text, x, y);
}
function drawPen(ctx) {
if (pointQueue.length >= 2) {
ctx.beginPath();
ctx.moveTo(...pointQueue.shift());
while (pointQueue.length > (mouse.button ? 1 : 0)) { ctx.lineTo(...pointQueue.shift()) }
pointQueue.length && ctx.lineTo(...pointQueue[0]);
ctx.stroke();
}
}
function update(){
if (pointQueue.length) {
drawPen(drawing.ctx);
ctx.clearRect(0, 0, w, h);
ctx.drawImage(drawing, 0, 0);
pointQueue.length && drawMousePos(ctx);
canvas.style.cursor = "none";
} else { canvas.style.cursor = "crosshair" }
requestAnimationFrame(update);
}
function createImage(w, h){
const can = document.createElement("canvas");
can.width = w;
can.height = h;
can.ctx = can.getContext("2d");
return can;
}
function mouseEvents(e){
mouse.x = e.pageX - bounds.left - 2; // offset by 2 pixels for canvas border
mouse.y = e.pageY - bounds.top - 2;
if (e.type === "mousedown") { mouse.button = true }
if (mouse.button) { pointQueue.push([mouse.x , mouse.y]) }
if (e.type === "mouseup") { mouse.button = false }
}
canvas {
border : 2px solid black;
cursor: crosshair;
}
Click drag mouse to draw<br>
<canvas id="canvas" width="512" height="256"></canvas>
you could try binding a div or a span to your mouse and attach it an eventListener like 'mousemove', in this div you could have two spans like
<div id='mouseCoord" style="position: absolute; top:0,left:0">
<span id='mouseX'><span>
<span id='mouseY'><span>
</div>
The div #mouseCoord would have an absolute position, and on mouse move, you could update his top/left position
const mouseDiv = document.getElementById('mouseCoord')
const mouseX = document.getElementById('mouseX')
const mouseY = document.getElementById('mouseY')
mouseDiv.addEventListener('mousemove', e => {
mouseDiv.setAttribute('style', `left: ${e.offsetX};top: ${e.offsetY}`)
mouseY.innerText = `y: ${e.offsetY}`
mouseY.innerText = `x: ${e.offsetX}`
})
With this in place, you could add some mouse events to control when you want to display the coordinate like only when the mouse is moving etc ...
I hope this can helps

Pan around the canvas within bounds smoothly

Most other solutions I have found relate to images as the bounds and work through css, and I can't get them to work with a canvas.
I want to create the bounds by defining an arbitrary percent size of the canvas (e.g 2x - twice the size) that expands from the center of the canvas and then have the canvas view (red) move/pan around within the bounds (green) by mouse hovering inside the canvas - that it moves to all corners of the bounds from the center of the canvas.
like this behavior in terms of the direction with panning
http://jsfiddle.net/georgedyer/XWnUA/2/
Additionally, I would like
the solution to work with a responsive canvas sizes rather than fixed canvas sizes
the panning to be smooth using easing such as CubicInOut easing
to detect the mouse hover inside the circle drawn. Might have to change the coordinate from screen to world to get it to work properly. But maybe theirs an easier way?
formulas
Here's my attempt at figuring out the formulas to get it working! I'll just focus on just one axis (x-axis) from left to right of the mouse, but the same formulas should apply to the y-axis.
First I get the "percentX" of the mouse along the width. If the "percentX" is 0 then the "newX" for the pan will be the value of the leftSide, else if it's 1 then it'll be the rightSide of the boundsBorder (green). So the "percentX" determines which side of the boundsBorder/limit the canvas view will extend/move to.
Only problem is that this doesn't work. So my formula must be wrong. The mouse inside the circle event is also inaccurate because of my canvas translate. Combining everything together is difficult to figure out!
Code
<html>
<head>
<meta charset="UTF-8">
<title>Panning</title>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
body {
background: #eee;
padding: 20px 0;
}
.canvas-container {
height: 400px;
width: 100%;
margin-bottom: 40px;
border-radius: 10px;
background: #fff;
overflow: hidden;
}
canvas {
width: 100%;
height: inherit;
}
.container {
width: 60%;
margin: 0 auto;
}
</style>
</head>
<body>
<!-- CANVAS -->
<div class="container">
<div class="canvas-container">
<canvas id="canvas"></canvas>
</div>
</div>
<!-- SCRIPT -->
<script type="text/javascript">
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var width = canvas.width = canvas.clientWidth;
var height = canvas.height = canvas.clientHeight;
var panX = 0, panY = 0;
var scaleBorderFactor = 2; /* scale factor for the bounds */
var mouse = {
x: 0,
y: 0
}
function Circle(x, y, radius, color){
this.x = x;
this.y = y;
this.radius = radius;
this.color = color;
this.draw = function(ctx){
ctx.beginPath();
ctx.arc(this.x,this.y,this.radius,0,Math.PI*2,false);
ctx.fillStyle = this.color;
ctx.fill();
ctx.closePath();
}
}
/* Rect Stroke Borders for visual reference */
function RectBorder(x, y, w, h, c){
this.x = x;
this.y = y;
this.width = w;
this.height = h;
this.color = c;
this.draw = function(ctx){
ctx.strokeStyle = this.color;
ctx.lineWidth = 2;
ctx.strokeRect(this.x, this.y, this.width, this.height);
};
/* Draw rect from a center point */
this.drawAtCenter = function(ctx){
ctx.strokeStyle = this.color;
ctx.lineWidth = 2;
ctx.strokeRect(this.x, this.y, this.width, this.height);
};
this.toString = function(){
console.log("x: "+this.x+", y: "+this.y+", w: "+this.width+", h: "+this.height+", color = "+this.color);
}
}
function getMousePos(canvas, event) {
var rect = canvas.getBoundingClientRect();
return {
x: (event.clientX - rect.left),
y: (event.clientY - rect.top)
};
}
function insideCircle(circle, mouse){
var dist = Math.sqrt(Math.pow(circle.x - mouse.x,2) + Math.pow(circle.y - mouse.y,2));
console.log(circle.radius+", d = "+dist)
return dist <= circle.radius;
}
function lerp(start, end, percent){
return start*(1 - percent) + percent*end;
}
/* t: current time, b: beginning value, c: change in value, d: duration */
function easeInOutCubic(t, b, c, d) {
if ((t/=d/2) < 1) return c/2*t*t*t + b;
return c/2*((t-=2)*t*t + 2) + b;
}
var canvasBorder = new RectBorder(0,0,canvas.width,canvas.height,'red');
var boundsBorder = new RectBorder(-(canvas.width*scaleBorderFactor - canvas.width)/2,-(canvas.height*scaleBorderFactor - canvas.height)/2,canvas.width*scaleBorderFactor,canvas.height*scaleBorderFactor,'green');
var circle = new Circle(200,200,40,'blue');
/* Draw Update */
update();
function update(){
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
width = canvas.width = canvas.clientWidth;
height = canvas.height = canvas.clientHeight;
ctx.clearRect(0,0,width,height);
ctx.save();
/* MOUSE */
percentX = (mouse.x / width);
percentY = (mouse.y / height);
/* the 2 sides of boundsBorder */
var leftSide = (width*scaleBorderFactor - width)/2 - width;
var rightSide = (width*scaleBorderFactor - width)/2 + width;
var topSide = (height*scaleBorderFactor - height)/2 - width;
var bottomSide = (height*scaleBorderFactor - height)/2 + height;
newX = rightSide * percentX + leftSide * (1 - percentX);
newY = bottomSide * percentY + topSide * (1 - percentY);
/* maybe use easeInOutCubic for better smoothness */
panX = lerp(-newX, mouse.x, 0.1);
panY = lerp(-newY, mouse.y, 0.1);
if (insideCircle(circle, mouse)){
circle.color = "pink";
} else {
circle.color = "blue";
}
ctx.translate(panX,panY);
/* Draw both borders only for reference */
canvasBorder.draw(ctx);
boundsBorder.drawAtCenter(ctx);
/* Draw Circle */
circle.draw(ctx);
ctx.restore();
requestAnimationFrame(update);
}
/* Events */
function mousemove(e) {
mouse = getMousePos(canvas, e);
}
/* Event Listeners */
canvas.addEventListener('mousemove', mousemove);
</script>
</body>
</html>
EDIT
I have managed to get the pan functionality to work. I just realized that all I need to do is offset by the positive and negative "widthGap"(see picture below) amount when panning. So it goes from positive "widthGap" (moves to the rght) to negative "widthGap" (moves to the left) when the "percentX" changes value like mentioned previously.
To get the smooth movement, I instead applied the lerp to the "percentX" values.
The code below is what should be replaced from the previous code above. It also shows the new variables I defined, which will make some of the variables from the previous code redundant.
UPDATED Code
var canvasBorder = new RectBorder(0,0,canvas.width,canvas.height,'red');
/* widthGap is the new leftSide formula.
* it is the gap differance between border and canvas
*/
var widthGap = (canvas.width*scaleBorderFactor - canvas.width)/2;
var heightGap = (canvas.height*scaleBorderFactor - canvas.height)/2;
var boundsBorder = new RectBorder(-widthGap,-heightGap,canvas.width+widthGap*2,canvas.height+heightGap*2,'green');
var circle = new Circle(200,200,40,'blue');
/* Draw Update */
update();
var newPercentX = 0, newPercentY = 0;
var percentX = 0, percentY = 0;
function update(){
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
width = canvas.width = canvas.clientWidth;
height = canvas.height = canvas.clientHeight;
ctx.clearRect(0,0,width,height);
ctx.save();
newPercentX = (mouse.x / width);
newPercentY = (mouse.y / height);
/* MOUSE */
percentX = lerp(percentX,newPercentX,0.05);
percentY = lerp(percentY,newPercentY,0.05);
panX = (widthGap) * percentX + (-widthGap) * (1 - percentX);
panY = (heightGap) * percentY + (-heightGap) * (1 - percentY);
ctx.translate(-panX,-panY);
if (insideCircle(circle, mouse)){
circle.color = "pink";
} else {
circle.color = "blue";
}
/* Draw both borders only for reference */
canvasBorder.draw(ctx);
boundsBorder.drawAtCenter(ctx);
/* Draw Circle */
circle.draw(ctx);
ctx.restore();
requestAnimationFrame(update);
}

Rotation Matrix Spiraling inward

I am trying to make a square that rotates in place, however my square is spiraling inward, and I have no idea why. Here is the code, if someone could please explain what is happening as to why it is not just spinning in place.
var angle = 2 * (Math.PI / 180);
var rotate = [
[Math.cos(angle),Math.sin(angle)],
[-Math.sin(angle),Math.cos(angle)]
];
var points = [[300,0],[0,300],[-300,0],[0,-300]];
init.ctx.translate(init.canvas.width/2,init.canvas.height/2);
function loop(){
draw();
}
setInterval(loop,10);
function draw(){
init.ctx.beginPath();
init.ctx.moveTo(points[0][0],points[0][1]);
init.ctx.lineTo(points[1][0],points[1][1]);
init.ctx.lineTo(points[2][0],points[2][1]);
init.ctx.lineTo(points[3][0],points[3][1]);
init.ctx.closePath();
init.ctx.stroke();
for(let i=0;i<points.length;i++){
init.ctx.beginPath();
init.ctx.fillStyle = "red";
init.ctx.fillRect(points[i][0],points[i][1],5,5);
points[i][0] = points[i][0]*rotate[0][0] + points[i][1]*rotate[0][1];
points[i][1] = points[i][0]*rotate[1][0] + points[i][1]*rotate[1][1];
}
}
So, you are applying a small rotation each time draw is called, specifically 1/180th of a full rotation. Problem is that you are relying on floating point math to give you exact values, and it's not because it doesn't. This is compounded by the points array being calculated by iterations. I suggest calculate the new points on each step through draw by applying the correct rotate matrix for your current angle to the starting points.
var angle = 0;
var startPoints = [[300,0],[0,300],[-300,0],[0,-300]];
var points = [[300,0],[0,300],[-300,0],[0,-300]];
init.ctx.translate(init.canvas.width/2,init.canvas.height/2);
function loop(){
draw();
}
setInterval(loop,10);
function draw(){
init.ctx.beginPath();
init.ctx.moveTo(points[0][0],points[0][1]);
init.ctx.lineTo(points[1][0],points[1][1]);
init.ctx.lineTo(points[2][0],points[2][1]);
init.ctx.lineTo(points[3][0],points[3][1]);
init.ctx.closePath();
init.ctx.stroke();
angle = angle + Math.PI / 90;
var rotate = [
[Math.cos(angle),Math.sin(angle)],
[-Math.sin(angle),Math.cos(angle)]
];
for(let i=0;i<points.length;i++){
init.ctx.beginPath();
init.ctx.fillStyle = "red";
init.ctx.fillRect(points[i][0],points[i][1],5,5);
points[i][0] = startPoints[i][0]*rotate[0][0] + startPoints[i][1]*rotate[0][1];
points[i][1] = startPoints[i][0]*rotate[1][0] + startPoints[i][1]*rotate[1][1];
}
}
Some tips to improve your code.
As a beginner I can see some bad habits creeping in and as there is already an answer I thought I would just give some tips to improve your code.
Don't use setInterval to create animations. requestAnimationFrame gives much better quality animations.
Arrays were created in high level languages to make life easier, not harder.
You have painfully typed out
init.ctx.beginPath();
init.ctx.moveTo(points[0][0],points[0][1]);
init.ctx.lineTo(points[1][0],points[1][1]);
init.ctx.lineTo(points[2][0],points[2][1]);
init.ctx.lineTo(points[3][0],points[3][1]);
init.ctx.closePath();
init.ctx.stroke();
That would be a nightmare if you had 100 points. Much better to create a generic function to do that for you.
function drawShape(ctx,shape){
ctx.beginPath();
for(var i = 0; i < shape.length; i++){
ctx.lineTo(shape[i][0], shape[i][1]);
}
ctx.closePath();
ctx.stroke();
}
Now you can render any shape on any canvas context with the same code.
drawShape(init.ctx,points); // how to draw your shape.
If you use a uniform scale then you can shorten the transform a little by reusing the x axis of the transformation
var rotate = [
[Math.cos(angle),Math.sin(angle)],
[-Math.sin(angle),Math.cos(angle)]
];
Note how the second two values are just the first two swapped with the new x negated. You can also include a scale in that and just hold the first two values.
var angle = ?
var scale = 1; // can be anything
// now you only need two values for the transform
var xAx = Math.cos(angle) * scale; // direction and size of x axis
var xAy = Math.sin(angle) * scale;
And you apply the transform to a point as follows
var px = ?; // point to transform
var py = ?;
var tx = px * xAx - py * xAy;
var ty = px * xAy + py * xAx;
And to add a origin
var tx = px * xAx - py * xAy + ox; // ox,oy is the origin
var ty = px * xAy + py * xAx + oy;
But is is much better to let the canvas 2D API do the transformation for you. The example below shows the various methods described above to render your box and animate the box.
Example using best practice.
const ctx = canvas.getContext("2d");
var w = canvas.width; // w,h these are set if canvas is resized
var h = canvas.height;
var cw = w / 2; // center width
var ch = h / 2; // center height
var globalScale = 1; // used to scale shape to fit the canvas
var globalTime;
var angle = Math.PI / 2;
var rotateRate = 90; // deg per second
var points = [
[300, 0],
[0, 300],
[-300, 0],
[0, -300]
];
var maxSize = Math.hypot(600, 600); // diagonal size used to caculate scale
// so that shape fits inside the canvas
// Add path to the current path
// shape contains path points
// x,y origin of shape
// scale is the scale of the shape
// angle is the amount of rotation in radians.
function createShape(shape, x, y, scale, angle) {
var i = 0;
ctx.setTransform(scale, 0, 0, scale, x, y); // set the scale and origin
ctx.rotate(angle); // set the rotation
ctx.moveTo(shape[i][0], shape[i++][1]);
while (i < shape.length) { // create a line to each point
ctx.lineTo(shape[i][0], shape[i++][1]);
}
}
// draws fixed scale axis aligned boxes at vertices.
// shape contains the vertices
// vertSize size of boxes drawn at verts
// x,y origin of shape
// scale is the scale of the shape
// angle is the amount of rotation in radians.
function drawVertices(shape, vertSize, x, y, scale, angle) {
ctx.setTransform(1, 0, 0, 1, x, y);
const xAx = Math.cos(angle) * scale; // direction and size of x axis
const xAy = Math.sin(angle) * scale;
var i = 0;
while (i < shape.length) {
const vx = shape[i][0]; // get vert coordinate
const vy = shape[i++][1]; // IMPORTANT DONT forget i++ in the while loop
ctx.fillRect(
vx * xAx - vy * xAy - vertSize / 2, // transform and offset by half box size
vx * xAy + vy * xAx - vertSize / 2,
vertSize, vertSize
);
}
}
// draws shape outline and vertices
function drawFullShape(shape, scale, angle, lineCol, vertCol, lineWidth, vertSize) {
// draw outline of shape
ctx.strokeStyle = lineCol;
ctx.lineWidth = lineWidth / scale; // to ensure that the line with is 1 pixel
// set the width to in inverse scale
ctx.beginPath();
// shape origin at cw,ch
createShape(shape, cw, ch, scale, angle);
ctx.closePath();
ctx.stroke();
// draw the vert boxes.
ctx.fillStyle = vertCol;
drawVertices(shape, vertSize, cw, ch, scale, angle);
}
function loop(timer) {
globalTime = timer;
if (w !== innerWidth || h !== innerHeight) { // check if canvas need resize
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
globalScale = Math.min(w / maxSize, h / maxSize);
}
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
ctx.clearRect(0, 0, w, h);
const innerAngle = globalTime * (rotateRate * (Math.PI / 180)) / 1000;
drawFullShape(points, globalScale, angle, "black", "red", 2, 6);
drawFullShape(points, globalScale * 0.5, innerAngle, "black", "red", 2, 6);
requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>

Scribing a cicrular line in Canvas

I am attempting to mouse-drag a dot around the outer perimeter of a large circle and have that dot appear to scribe a thick line around the outer perimeter behind itself. I can get everything to work except scribing the outer line behind the dot. I have researched many ideas and tried many of my own but the line still produces "spotted" results. Here is an image to show what I'm attempting.
MounseDrag Scribed Line
Thank you for taking the time to read my question. :-)
<script type="text/javascript">
var canvas1 = document.getElementById("canvas1"),
canvas2 = document.getElementById("canvas2"),
c1 = canvas1.getContext("2d"),
c2 = canvas2.getContext("2d"),
dot = 7,
started = false,
width = 350,
height = 350,
radians = 0,
cRad = 165, // Circle Radius
cord = {mX:0, mY:0, csX:0, snY:0, x:0, y:0},
init = function(){
cord.mX = 0;
cord.mY = 0;
cord.csX = width /2 + cRad;
cord.snY = height /2;
cord.x = width /2;
cord.y = height /2;
};
init();
canvas1.width = width;
canvas1.height = height;
canvas2.width = width;
canvas2.height = height;
canvas1.addEventListener("mousemove", function(event) {
cord.mX = event.clientX - canvas1.offsetLeft;
cord.mY = event.clientY - canvas1.offsetTop;
});
canvas1.addEventListener("mousedown", function(event) {
if (started) {
started = false;
} else {
started = true;
render();
};
});
function update() {
radians = Math.atan2(cord.mY - width/2, cord.mX - height/2);
cord.csX = width/2 - Math.cos(radians) * cRad * -1;
cord.snY = height/2 - Math.sin(radians) * cRad * -1;
};
function outerTheta() {
c2.beginPath();
c2.arc(cord.csX, cord.snY, 3, 0, Math.PI * 2);
c2.closePath();
c2.fillStyle = "#000";
c2.fill();
};
function render() {
c1.clearRect(0, 0, width, height);
c1.beginPath();
c1.moveTo(cord.x, cord.y);
c1.lineTo(cord.csX, cord.snY);
c1.lineWidth = 3;
c1.strokeStyle = "#000";
c1.stroke();
c1.beginPath(); //<---------------------------------- Drag-Dot
c1.arc(cord.csX, cord.snY, dot, 0, Math.PI * 2);
c1.closePath();
c1.fillStyle = "#000";
c1.fill();
if(started){
update();
outerTheta();
requestAnimationFrame(render);
};
};
render();
</script>
The browser is not able to cycle the animation as quickly as the mouse is moving. If you move the mouse slowly, then the dots that are drawn in each animation cycle overlap and the circle has a solid line. If you move the mouse quickly, then the dots do not overlap and you get "spotting".
If you pay close attention to the way drawing programs work, you will see that the "pen" tool draws a continuous line. If you move the mouse quickly while using the tool, the continuous line is made up of line segments that stretch from each point that the computer was able to capture while your mouse was moving quickly.
I modified your program so that a line segment stretches between each captured point during the animation cycle:
https://jsfiddle.net/17hvw5pp
var canvas1 = document.getElementById("canvas1"),
canvas2 = document.getElementById("canvas2"),
c1 = canvas1.getContext("2d"),
c2 = canvas2.getContext("2d"),
dot = 7,
started = false,
width = 350,
height = 350,
radians = 0,
cRad = 165, // Circle Radius
cord = {mX:0, mY:0, csX:0, snY:0, x:0, y:0},
init = function(){
cord.mX = 0;
cord.mY = 0;
cord.csX = width /2 + cRad;
cord.snY = height /2;
cord.lastCSX = cord.csX;
cord.lastSNY = cord.snY;
cord.x = width /2;
cord.y = height /2;
};
canvas1.style.position="absolute";
canvas2.style.position="absolute";
init();
canvas1.width = width;
canvas1.height = height;
canvas2.width = width;
canvas2.height = height;
canvas1.addEventListener("mousemove", function(event) {
cord.mX = event.clientX - canvas1.offsetLeft;
cord.mY = event.clientY - canvas1.offsetTop;
});
canvas1.addEventListener("mousedown", function(event) {
if (started) {
started = false;
} else {
started = true;
render();
};
});
function update() {
radians = Math.atan2(cord.mY - width/2, cord.mX - height/2);
cord.csX = width/2 - Math.cos(radians) * cRad * -1;
cord.snY = height/2 - Math.sin(radians) * cRad * -1;
};
function outerTheta() {
//draw a line from the last known coordinate to the new known coordinate
c2.beginPath();
c2.moveTo(cord.lastCSX, cord.lastSNY);
c2.lineTo(cord.csX, cord.snY);
c2.lineWidth=5;
c2.strokeStyle="#000";
c2.stroke();
cord.lastCSX = cord.csX;
cord.lastSNY = cord.snY;
c2.beginPath();
c2.arc(cord.csX, cord.snY, 3, 0, Math.PI * 2);
c2.closePath();
c2.fillStyle = "#000";
c2.fill();
};
function render() {
c1.clearRect(0, 0, width, height);
c1.beginPath();
c1.moveTo(cord.x, cord.y);
c1.lineTo(cord.csX, cord.snY);
c1.lineWidth = 3;
c1.strokeStyle = "#000";
c1.stroke();
c1.beginPath(); //<---------------------------------- Drag-Dot
c1.arc(cord.csX, cord.snY, dot, 0, Math.PI * 2);
c1.closePath();
c1.fillStyle = "#000";
c1.fill();
if(started){
update();
outerTheta();
requestAnimationFrame(render);
};
};
render();
This works better, but not perfectly: If you move the mouse quickly, the line segment will become a chord across the circle and this ruins the effect.
I attempted to modify the program to draw an arc between the two known points:
https://jsfiddle.net/17hvw5pp/1/
You can see that this implementation is also not ideal because the arc function becomes confused about which direction to draw the partial circle based on just two radians coordinates. Using quaternion math will solve this problem for you.
https://en.wikipedia.org/wiki/Quaternion
But that may be more complication that you want to introduce into this project.

How to test if a point is in a rectangle area which rotates an angle?

I am trying to test if a point is inside a rectangle area that rotates an angle around (x, y), like the image below. This is language agnostic problem but I am working with HTML5 canvas now.
Suppose the point we need to test is (x1, y1), the width of the rectangle is 100 and the height is 60. In normal cartesian coordinate system the rectangle ABCD top left point A is (canvas.width / 2, canvas.height / 2 -rect.height/2). I assume that (canvas.width / 2, canvas.height / 2) is at the middle of line AB where B is (canvas.width / 2, canvas.height / 2 + rect.height /2).
I have read some resources here and wrote a test project, but it doesn't test the correct area. In my test project I want the this effect:
if the mouse is on a point that is within the range of the testing rectangle area a dot will be displayed around the mouse. If it is outside the rectangle nothing will be displayed.
However my test project looks like this: (Note that although I used the vector based technique to test the point in a rotated rectangle area, the test area remains the rectangle before rotation)
// Detecting a point is in a rotated rectangle area
// using vector based method
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
class Rectangle {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.searchPoint = { x: 0, y: 0};
this.binding();
}
binding() {
let self = this;
window.addEventListener('mousemove', e => {
if (!e) return;
let rect = canvas.getBoundingClientRect();
let mx = e.clientX - rect.left - canvas.clientLeft;
let my = e.clientY - rect.top - canvas.clientTop;
self.searchPoint = { x: mx, y: my };
});
}
}
let rect = new Rectangle(canvas.width /2, canvas.height /2 - 30, 100, 60);
function vector(p1, p2) {
return {
x: (p2.x - p1.x),
y: (p2.y - p1.y)
};
}
function point(x, y) {
return { x, y };
}
// Vector dot operation
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function pointInRect(p, rect, angle) {
let a = newPointTurningAngle(0, -rect.height / 2, angle);
let b = newPointTurningAngle(0, rect.height / 2, angle);
let c = newPointTurningAngle(rect.width, rect.height / 2, angle);
let AB = vector(a, b);
let AM = vector(a, p);
let BC = vector(b, c);
let BM = vector(b, p);
let dotABAM = dot(AB, AM);
let dotABAB = dot(AB, AB);
let dotBCBM = dot(BC, BM);
let dotBCBC = dot(BC, BC);
return 0 <= dotABAM && dotABAM <= dotABAB && 0 <= dotBCBM && dotBCBM <= dotBCBC;
}
function drawLine(x, y) {
ctx.strokeStyle = 'black';
ctx.lineTo(x, y);
ctx.stroke();
}
function text(text, x, y) {
ctx.font = "18px serif";
ctx.fillText(text, x, y);
}
function newPointTurningAngle(nx, ny, angle) {
return {
x: nx * Math.cos(angle) - ny * Math.sin(angle),
y: nx * Math.sin(angle) + ny * Math.cos(angle)
};
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.moveTo(canvas.width / 2, 0);
drawLine(canvas.width /2, canvas.height / 2);
ctx.moveTo(0, canvas.height / 2);
drawLine(canvas.width / 2, canvas.height /2);
let angle = -Math.PI / 4;
ctx.setTransform(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), canvas.width / 2, canvas.height / 2);
//ctx.setTransform(1, 0, 0, 1, canvas.width/2, canvas.height / 2);
ctx.strokeStyle = 'red';
ctx.strokeRect(0, -rect.height / 2, rect.width, rect.height);
let p = newPointTurningAngle(rect.searchPoint.x - canvas.width / 2, rect.searchPoint.y - canvas.height / 2, angle);
let testResult = pointInRect(p, rect, angle);
if (testResult) {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.arc(rect.searchPoint.x, rect.searchPoint.y, 5, 0, Math.PI * 2);
ctx.fill();
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
text('searchPoint x: ' + rect.searchPoint.x + ', y: ' + rect.searchPoint.y, 60, 430);
text('x: ' + canvas.width / 2 + ', y: ' + canvas.height / 2, 60, 480);
requestAnimationFrame(animate);
}
animate();
<canvas id='canvas'></canvas>
Updated Solution
I am still using the vector based method as followed:
0 <= dot(AB,AM) <= dot(AB,AB) &&
0 <= dot(BC,BM) <= dot(BC,BC)
Now I have changed the point's rotated angle and the corner point coordinates so the point can be detected in the rectangle. The corner points are already in the rotated coordinate system so they don't need to be translated, however the point of the mouse location needs to be translated before testing it in the rectangle area.
In setTransform method the angle rotated is positive when rotated clockwise, the form is :
ctx.setTransform(angle_cosine, angle_sine, -angle_sine, angle_cosine, x, y);
So when calculating the point's new coordinate after rotating an angle, the formula need to change to this so that the angle is also positive when rotated clockwise:
new_x = x * angle_cosine + y * angle_sine;
new_y = -x * angle_sine + y * angle_cos;
// Detecting a point is in a rotated rectangle area
// using vector based method
const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const ctx = canvas.getContext('2d');
class Rectangle {
constructor(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
this.searchPoint = { x: 0, y: 0};
this.binding();
}
binding() {
let self = this;
window.addEventListener('mousemove', e => {
if (!e) return;
let rect = canvas.getBoundingClientRect();
let mx = e.clientX - rect.left - canvas.clientLeft;
let my = e.clientY - rect.top - canvas.clientTop;
self.searchPoint = { x: mx, y: my };
});
}
}
let rect = new Rectangle(canvas.width /2, canvas.height /2 - 30, 100, 60);
function vector(p1, p2) {
return {
x: (p2.x - p1.x),
y: (p2.y - p1.y)
};
}
function point(x, y) {
return { x, y };
}
// Vector dot operation
function dot(a, b) {
return a.x * b.x + a.y * b.y;
}
function pointInRect(p, rect) {
let a = { x: 0, y: -rect.height / 2};
let b = { x: 0, y: rect.height / 2};
let c = { x: rect.width, y: rect.height / 2};
text('P x: ' + p.x.toFixed() + ', y: ' + p.y.toFixed(), 60, 430);
text('A x: ' + a.x.toFixed() + ', y: ' + a.y.toFixed(), 60, 455);
text('B x: ' + b.x.toFixed() + ', y: ' + b.y.toFixed(), 60, 480);
let AB = vector(a, b);
let AM = vector(a, p);
let BC = vector(b, c);
let BM = vector(b, p);
let dotABAM = dot(AB, AM);
let dotABAB = dot(AB, AB);
let dotBCBM = dot(BC, BM);
let dotBCBC = dot(BC, BC);
return 0 <= dotABAM && dotABAM <= dotABAB && 0 <= dotBCBM && dotBCBM <= dotBCBC;
}
function drawLine(x, y) {
ctx.strokeStyle = 'black';
ctx.lineTo(x, y);
ctx.stroke();
}
function text(text, x, y) {
ctx.font = "18px serif";
ctx.fillText(text, x, y);
}
function newPointTurningAngle(nx, ny, angle) {
let cos = Math.cos(angle);
let sin = Math.sin(angle);
return {
x: nx * cos + ny * sin,
y: -nx * sin + ny * cos
};
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.moveTo(canvas.width / 2, 0);
drawLine(canvas.width /2, canvas.height / 2);
ctx.moveTo(0, canvas.height / 2);
drawLine(canvas.width / 2, canvas.height /2);
let angle = - Math.PI / 4;
ctx.setTransform(Math.cos(angle), Math.sin(angle), -Math.sin(angle), Math.cos(angle), canvas.width / 2, canvas.height / 2);
ctx.strokeStyle = 'red';
ctx.strokeRect(0, -rect.height / 2, rect.width, rect.height);
let p = newPointTurningAngle(rect.searchPoint.x - canvas.width / 2, rect.searchPoint.y - canvas.height / 2, angle);
ctx.setTransform(1, 0, 0, 1, 0, 0);
let testResult = pointInRect(p, rect);
if (testResult) {
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.arc(rect.searchPoint.x, rect.searchPoint.y, 5, 0, Math.PI * 2);
ctx.fill();
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
text('searchPoint x: ' + rect.searchPoint.x + ', y: ' + rect.searchPoint.y, 60, 412);
text('x: ' + canvas.width / 2 + ', y: ' + canvas.height / 2, 60, 510);
requestAnimationFrame(animate);
}
animate();
<canvas id='canvas'></canvas>
Assuming that you know how to check whether a dot is in the rectangle the approach to solution is to rotate and translate everything (dot and rectangle) to "normalized" coordinating system (Cartesian coordinate system that is familiar to us) and then to check it trivially.
For more information you should check Affine transformations. The good link where you could start is
http://www.mathworks.com/discovery/affine-transformation.html?requestedDomain=www.mathworks.com
As you can see on this Codepen i did (to detect 2 rotate rect collide).
You have to check the 2 projections of your point (in my case, the 4 points of a rect) and look if the projections are on the other rect
You have to handle the same thing but only for a point and a rect instead of 2 rects
All projections are not colliding
All projections are colliding
required code for codepen link
The browser always report mouse position untransformed (==unrotated).
So to test if the mouse is inside a rotated rectangle, you can:
Get the unrotated mouse position from the mouse event (relative to the canvas).
Rotate the mouse x,y versus the rotation point by the same rotation as the rectangle.
Test if the mouse is inside the rectangle. Now that both the rect and the mouse position have been similarly rotated, you can just test as if the mouse and rect were unrotated.
Annotated code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
function reOffset(){
var BB=canvas.getBoundingClientRect();
offsetX=BB.left;
offsetY=BB.top;
}
var offsetX,offsetY;
reOffset();
window.onscroll=function(e){ reOffset(); }
window.onresize=function(e){ reOffset(); }
var isDown=false;
var startX,startY;
var rect=makeRect(50,20,35,20,Math.PI/4,60,30);
function makeRect(x,y,w,h,angle,rotationPointX,rotationPointY){
return({
x:x,y:y,width:w,height:h,
rotation:angle,rotationPoint:{x:rotationPointX,y:rotationPointY},
});
}
drawRect(rect);
$("#canvas").mousedown(function(e){handleMouseDown(e);});
function drawRect(r){
var rx=r.rotationPoint.x;
var ry=r.rotationPoint.y;
// demo only, draw the rotation point
dot(rx,ry,'blue');
// draw the rotated rect
ctx.translate(rx,ry);
ctx.rotate(r.rotation);
ctx.strokeRect(rect.x-rx,rect.y-ry,r.width,r.height);
// always clean up, undo the transformations (in reverse order)
ctx.rotate(-r.rotation);
ctx.translate(-rx,-ry);
}
function dot(x,y,fill){
ctx.fillStyle=fill;
ctx.beginPath();
ctx.arc(x,y,3,0,Math.PI*2);
ctx.fill();
}
function handleMouseDown(e){
// tell the browser we're handling this event
e.preventDefault();
e.stopPropagation();
// get mouse position relative to canvas
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// rotate the mouse position versus the rotationPoint
var dx=mouseX-rect.rotationPoint.x;
var dy=mouseY-rect.rotationPoint.y;
var mouseAngle=Math.atan2(dy,dx);
var mouseDistance=Math.sqrt(dx*dx+dy*dy);
var rotatedMouseX=rect.rotationPoint.x+mouseDistance*Math.cos(mouseAngle-rect.rotation);
var rotatedMouseY=rect.rotationPoint.y+mouseDistance*Math.sin(mouseAngle-rect.rotation);
// test if rotated mouse is inside rotated rect
var mouseIsInside=rotatedMouseX>rect.x &&
rotatedMouseX<rect.x+rect.width &&
rotatedMouseY>rect.y &&
rotatedMouseY<rect.y+rect.height;
// draw a dot at the unrotated mouse position
// green if inside rect, otherwise red
var hitColor=mouseIsInside?'green':'red';
dot(mouseX,mouseY,hitColor);
}
body{ background-color: ivory; }
#canvas{border:1px solid red; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Clicks inside rect are green, otherwise red.</h4>
<canvas id="canvas" width=512 height=512></canvas>

Categories

Resources