【问题标题】:Incorrectly drawing line to edge of ellipse given angle给定角度的椭圆边缘不正确地画线
【发布时间】:2017-01-25 03:20:54
【问题描述】:

抱歉标题混乱,我不知道如何简洁地描述我的问题。

我正在使用 javascript 在canvas 元素上绘制一个椭圆,并且我试图弄清楚如何检测鼠标是否在椭圆内单击。我试图做到这一点的方法是将椭圆中心到鼠标的距离与与鼠标点击角度相同的椭圆半径进行比较。这是一张可怕的图片,代表我刚才所说的,如果它仍然令人困惑:

显然这不起作用,否则我不会问这个,所以下面是计算出的半径线(红色)和鼠标线(蓝色)的图片。在这张图片中,鼠标以与椭圆中心成 45° 的角度单击,我计算出半径线以大约 34.99° 的角度绘制。

下面是计算代码:

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

我真的不确定为什么这不起作用,我一直在盯着这个数学,它似乎是正确的。它是正确的吗?在画布上绘画有什么东西使它不起作用?请帮忙!

【问题讨论】:

    标签: javascript math canvas geometry


    【解决方案1】:

    如果你有一个形式为 (x-x0)2/a2 + (y-y0)2/b2 = 1,则点 (x, y) 在椭圆内当且仅当 (x-x0) 2/a2 + (y-y0)2/b2

    为了能够在椭圆的边缘画一条线:使用 atan2 获取鼠标的 theta(不要使用 acos,你会在象限 III 和 IV 中得到不正确的结果),使用椭圆的极坐标方程求解r,然后转换回直角坐标并绘制。

    【讨论】:

    • 椭圆方程的形式为 (x-x0)^2/a^2 + (y-y0)^2/b^2 = 1
    • 谢谢你,这很有效,而且非常简单!
    【解决方案2】:

    椭圆线截距

    寻找截距包括解决该点是否在内部。

    如果是通过2D上下文绘制椭圆,解决方法如下

    // 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()
    

    在椭圆上找到与从中心到 x,y 的直线相交的点。为了解决这个问题,我将椭圆归一化,使其成为一个圆(移动线条,使椭圆在其坐标空间中成为一个圆)。

    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
    

    程序可以优化,但现在可以解决所提出的问题。

    指向里面

    然后判断该点是否在里面,只需比较鼠标与截取点的距离即可。

    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 
    }
    

    方法使用示例。

        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 **/

    【讨论】:

    • 抱歉,您能否扩展一些变量以使其名称更易于理解?很难理解ddx 和ddy 所代表的含义。
    • @SamPerlmutter (ddx, ddy) 表示旋转到归一化椭圆坐标空间的向量 (dx,dy)。归一化的椭圆空间是椭圆旋转回0并在一个方向上缩放成一个圆。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-11-07
    • 1970-01-01
    • 1970-01-01
    • 2012-03-13
    相关资源
    最近更新 更多