简体   繁体   中英

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>

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>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM