'Determine if mouse was clicked on a rotated rectangle on a canvas

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>


Solution 1:[1]

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>

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Blindman67