1
votes

Sorry for the confusing title, I don't know how to succinctly describe my question.

I'm drawing an ellipse on a canvas element using javascript and I'm trying to figure out how to detect if the mouse is clicked inside of the ellipse or not. The way I'm trying to do this is by comparing the distance from the center of the ellipse to the mouse to the radius of the ellipse at the same angle as the mouse click. Here's a terrible picture representing what I just said if it's still confusing: Visualization of click test

Obviously this isn't working, otherwise I wouldn't be asking this, so below is a picture of the computed radius line (in red) and the mouse line (in blue). In this picture, the mouse has been clicked at a 45° angle to the center of the ellipse and I've calculated that the radius line is being drawn at about a 34.99° angle.

Visualization of incorrect radius calculation

And below is the calculation code:

//This would be the blue line in the picture above
var mouseToCenterDistance = distanceTo(centerX, centerY, mouseX, mouseY);
var angle = Math.acos((mouseX - centerX) / mouseToCenterDistance);
var radiusPointX = (radiusX * Math.cos(angle)) + centerX;
var radiusPointY = (radiusY * Math.sin(-angle)) + centerY;

//This would be the red line in the picture above
var radius = distanceTo(centerX, centerY, radiusPointX, radiusPointY);

var clickedInside = mouseToCenterDistance <= radius;

I'm really not sure why this isn't working, I've been staring at this math forever and it seems correct. Is it correct and there's something about drawing on the canvas that's making it not work? Please help!

2

2 Answers

1
votes

If you have an ellipse of the form (x-x0)2/a2 + (y-y0)2/b2 = 1, then a point (x, y) is inside the ellipse if and only if (x-x0)2/a2 + (y-y0)2/b2 < 1. You can just test that inequality to see if the mouse is inside the ellipse.

To be able to draw a line to the edge of the ellipse: get the theta of the mouse with atan2 (don't use acos, you'll get incorrect results in quadrants III & IV), use the polar equation of the ellipse to solve for r, then convert back to rectangular coordinates and draw.

0
votes

Ellipse line intercept

Finding the intercept includes solving if the point is inside.

If it is the ellipse draw via the 2D context the solution is as follows

// defines the ellipse
var cx = 100;  // center
var cy = 100;
var r1 = 20;  // radius 1
var r2 = 100; // radius 2
var ang = 1;  // angle in radians

// rendered with
ctx.beginPath();
ctx.ellipse(cx,cy,r1,r2,ang,0,Math.PI * 2,true)
ctx.stroke()

To find the point on the ellipse that intersects the line from the center to x,y. To solve I normalise the ellipse so that it is a circle (well the line is moved so that the ellipse is a circle in its coordinate space).

var x = 200;
var y = 200;
var ratio = r1 / r2; // need the ratio between the two radius

//  get the vector from the ellipse center to end of line
var dx = x - cx;
var dy = y - cy;

// get the vector that will normalise the ellipse rotation
var vx = Math.cos(-ang);
var vy = Math.sin(-ang);

// use that vector to rotate the line 
var ddx = dx * vx - dy * vy;
var ddy = (dx * vy + dy * vx) * ratio;  // lengthen or shorten dy

// get the angle to the line in normalise circle space.
var c = Math.atan2(ddy,ddx);

// get the vector along the ellipse x axis
var eAx = Math.cos(ang);
var eAy = Math.sin(ang);

// get the intercept of the line and the normalised ellipse
var nx = Math.cos(c) * r1;
var ny = Math.sin(c) * r2;

// rotate the intercept to the ellipse space
var ix = nx * eAx - ny * eAy
var iy = nx * eAy + ny * eAx

// cx,cy to ix ,iy is from the center to the ellipse circumference

The procedure can be optimised but for now that will solve the problem as presented.

Is point inside

Then to determine if the point is inside just compare the distances of the mouse and the intercept point.

var x = 200; // point to test
var y = 200;

//  get the vector from the ellipse center to point to test
var dx = x - cx;
var dy = y - cy;

// get the vector that will normalise the ellipse rotation
var vx = Math.cos(ang);
var vy = Math.sin(ang);

// use that vector to rotate the line 
var ddx = dx * vx + dy * vy;
var ddy = -dx * vy + dy * vx;

if( 1 >= (ddx * ddx) / (r1 * r1) + (ddy * ddy) / (r2 * r2)){
    // point on circumference or inside ellipse 
}

Example use of method.

    function path(path){
        ctx.beginPath();
        var i = 0;
        ctx.moveTo(path[i][0],path[i++][1]);
        while(i < path.length){
            ctx.lineTo(path[i][0],path[i++][1]);
        }
        if(close){
            ctx.closePath();
        }
        ctx.stroke();
    }
    function strokeCircle(x,y,r){
        ctx.beginPath();
        ctx.moveTo(x + r,y);
        ctx.arc(x,y,r,0,Math.PI * 2);
        ctx.stroke();
    }
    function display() { 
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transform
        ctx.globalAlpha = 1; // reset alpha
        ctx.clearRect(0, 0, w, h);
        var cx = w/2;
        var cy = h/2;
        var r1 = Math.abs(Math.sin(globalTime/ 4000) * w / 4);
        var r2 = Math.abs(Math.sin(globalTime/ 4300) * h / 4);
        var ang = globalTime / 1500;
    
    
        // find the intercept from ellipse center to mouse on the ellipse 
        var ratio = r1 / r2
    
        var dx = mouse.x - cx;
        var dy = mouse.y - cy;
        var dist = Math.hypot(dx,dy);
        var ex = Math.cos(-ang);
        var ey = Math.sin(-ang);
        var c = Math.atan2((dx * ey + dy * ex) * ratio, dx * ex - dy * ey);
        var nx = Math.cos(c) * r1;
        var ny = Math.sin(c) * r2;
        var ix = nx * ex + ny * ey;
        var iy = -nx * ey + ny * ex;
        var dist = Math.hypot(dx,dy);
        var dist2Inter = Math.hypot(ix,iy);
        
        ctx.strokeStyle = "Blue";
        ctx.lineWidth = 4;
        ctx.beginPath();
        ctx.ellipse(cx,cy,r1,r2,ang,0,Math.PI * 2,true)
        ctx.stroke();
        
        if(dist2Inter > dist){
            ctx.fillStyle = "#7F7";
            ctx.globalAlpha = 0.5;
            ctx.fill();
            ctx.globalAlpha = 1;
            
        }
    
        
        // Display the intercept
        ctx.strokeStyle = "black";
        ctx.lineWidth = 2;
        path([[cx,cy],[mouse.x,mouse.y]])
        ctx.strokeStyle = "red";
        ctx.lineWidth = 5;
        path([[cx,cy],[cx + ix,cy+iy]])
        ctx.strokeStyle = "red";
        ctx.lineWidth = 4;
        strokeCircle(cx + ix, cy + iy, 6)
        ctx.fillStyle = "white";
        ctx.fill();
        ctx.strokeStyle = "red";
        ctx.lineWidth = 4;
        strokeCircle(cx, cy, 6)
        ctx.fillStyle = "white";
        ctx.fill();        
        
        ctx.strokeStyle = "black";
        ctx.lineWidth = 2;
        strokeCircle(mouse.x, mouse.y, 4)
        ctx.fillStyle = "white";
        ctx.fill();   
    }
    
    
    
    /** SimpleFullCanvasMouse.js begin **/
    //==============================================================================
    // Boilerplate code from here down and not related to the answer
    //==============================================================================
    var w, h, cw, ch, canvas, ctx, mouse, globalTime = 0, firstRun = true;
    ;(function(){
        const RESIZE_DEBOUNCE_TIME = 100;
        var  createCanvas, resizeCanvas, setGlobals, resizeCount = 0;
        createCanvas = function () {
            var c,
            cs;
            cs = (c = document.createElement("canvas")).style;
            cs.position = "absolute";
            cs.top = cs.left = "0px";
            cs.zIndex = 1000;
            document.body.appendChild(c);
            return c;
        }
        resizeCanvas = function () {
            if (canvas === undefined) {
                canvas = createCanvas();
            }
            canvas.width = innerWidth;
            canvas.height = innerHeight;
            ctx = canvas.getContext("2d");
            if (typeof setGlobals === "function") {
                setGlobals();
            }
            if (typeof onResize === "function") {
                if(firstRun){
                    onResize();
                    firstRun = false;
                }else{
                    resizeCount += 1;
                    setTimeout(debounceResize, RESIZE_DEBOUNCE_TIME);
                }
            }
        }
        function debounceResize() {
            resizeCount -= 1;
            if (resizeCount <= 0) {
                onResize();
            }
        }
        setGlobals = function () {
            cw = (w = canvas.width) / 2;
            ch = (h = canvas.height) / 2;
        }
        mouse = (function () {
            function preventDefault(e) {
                e.preventDefault();
            }
            var mouse = {
                x : 0,
                y : 0,
                w : 0,
                alt : false,
                shift : false,
                ctrl : false,
                buttonRaw : 0,
                over : false,
                bm : [1, 2, 4, 6, 5, 3],
                active : false,
                bounds : null,
                crashRecover : null,
                mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
            };
            var m = mouse;
            function mouseMove(e) {
                var t = e.type;
                m.bounds = m.element.getBoundingClientRect();
                m.x = e.pageX - m.bounds.left;
                m.y = e.pageY - m.bounds.top;
                m.alt = e.altKey;
                m.shift = e.shiftKey;
                m.ctrl = e.ctrlKey;
                if (t === "mousedown") {
                    m.buttonRaw |= m.bm[e.which - 1];
                } else if (t === "mouseup") {
                    m.buttonRaw &= m.bm[e.which + 2];
                } else if (t === "mouseout") {
                    m.buttonRaw = 0;
                    m.over = false;
                } else if (t === "mouseover") {
                    m.over = true;
                } else if (t === "mousewheel") {
                    m.w = e.wheelDelta;
                } else if (t === "DOMMouseScroll") {
                    m.w = -e.detail;
                }
                if (m.callbacks) {
                    m.callbacks.forEach(c => c(e));
                }
                if ((m.buttonRaw & 2) && m.crashRecover !== null) {
                    if (typeof m.crashRecover === "function") {
                        setTimeout(m.crashRecover, 0);
                    }
                }
                e.preventDefault();
            }
            m.addCallback = function (callback) {
                if (typeof callback === "function") {
                    if (m.callbacks === undefined) {
                        m.callbacks = [callback];
                    } else {
                        m.callbacks.push(callback);
                    }
                }
            }
            m.start = function (element) {
                if (m.element !== undefined) {
                    m.removeMouse();
                }
                m.element = element === undefined ? document : element;
                m.mouseEvents.forEach(n => {
                    m.element.addEventListener(n, mouseMove);
                });
                m.element.addEventListener("contextmenu", preventDefault, false);
                m.active = true;
            }
            m.remove = function () {
                if (m.element !== undefined) {
                    m.mouseEvents.forEach(n => {
                        m.element.removeEventListener(n, mouseMove);
                    });
                    m.element.removeEventListener("contextmenu", preventDefault);
                    m.element = m.callbacks = undefined;
                    m.active = false;
                }
            }
            return mouse;
        })();
        // Clean up. Used where the IDE is on the same page.
        var done = function () {
            window.removeEventListener("resize", resizeCanvas)
            mouse.remove();
            document.body.removeChild(canvas);
            canvas = ctx = mouse = undefined;
        }
        function update(timer) { // Main update loop
            if(ctx === undefined){ return; }
            globalTime = timer;
            display(); // call demo code
            requestAnimationFrame(update);
        }
        setTimeout(function(){
            resizeCanvas();
            mouse.start(canvas, true);
            //mouse.crashRecover = done;
            window.addEventListener("resize", resizeCanvas);
            requestAnimationFrame(update);
        },0);
    })();
    /** SimpleFullCanvasMouse.js end **/