缩放和平移画布后,鼠标坐标不匹配

10

我对JavaScript和Canvas非常陌生,我有一个程序,它应该能够检测沿着椭圆路径动画的元素。它随后会形成一棵树。但这是我链接到jsfiddle的基本结构。没有缩放或平移时它可以正常工作,但一旦尝试进行缩放或平移,鼠标坐标就会失控。我尝试遵循markE在 HTML5 canvas get coordinates after zoom and translate中的建议,但我肯定做错了什么,我显然没有理解Canvas和变换矩阵发生了什么。我花了大约3天时间尝试更改我所能想到的所有组合,但似乎无法找到答案 :s

已解决:这里是我的代码,可以缩放和平移鼠标,用于在椭圆上动画和检测元素:http://jsfiddle.net/metalloyd/A8hgz/

            theCanvas = document.getElementById("canvasOne");
            context = theCanvas.getContext("2d");
            var status = document.getElementById('status');
            var $canvas = $("#canvasOne");
            var canvasOffset = $canvas.offset();
            var offsetX = canvasOffset.left;
            var offsetY = canvasOffset.top;
            var scrollX = $canvas.scrollLeft();
            var scrollY = $canvas.scrollTop();
            var cw = theCanvas.width;
            var ch = theCanvas.height;
            var scaleFactor = 1.00;
            var panX = 0;
            var panY = 0;

            var mainX = 250;
            // setting the middle point position X value
            var mainY = 100;
            // setting the middle point position Y value
            var mainR = 125;
            // main ellipse radius R
            var no = 5;
            // number of nodes to display
            var div_angle = 360 / no;

            var circle = {
                centerX: mainX,
                centerY: mainY + 100,
                radius: mainR,
                angle: .9
            };

            var ball = {
                x: 0,
                y: 0,
                speed: .1
            };
            var a = 1.8;
            //Ellipse width
            var b = .5;
            //Ellipse height

           //Scale and Pan variables
            var translatePos = {
                x: 1,
                y: 1
            };
            var startDragOffset = {};
            var mouseDown = false;

            var elements = [{}];

            // Animate
            var animateInterval = setInterval(drawScreen, 1);

            //Animation
            function drawScreen() {
                context.clearRect(0, 0, cw, ch);
                // Background box
                context.beginPath();
                context.fillStyle = '#EEEEEE';
                context.fillRect(0, 0, theCanvas.width, theCanvas.height);
                context.strokeRect(1, 1, theCanvas.width - 2, theCanvas.height - 2);
                context.closePath();

                context.save();
                context.translate(panX, panY);
                context.scale(scaleFactor, scaleFactor);

                ball.speed = ball.speed + 0.001;

                for (var i = 1; i <= no; i++) {
                    // male
                    new_angle = div_angle * i;
                    //Starting positions for ball 1 at different points on the ellipse
                    circle.angle = (new_angle * (0.0174532925)) + ball.speed;
                    //elliptical x position and y position for animation for the first ball
                    //xx and yy records the first balls coordinates
                    xx = ball.x = circle.centerX - (a * Math.cos(circle.angle)) * (circle.radius);
                    yy = ball.y = circle.centerY + (b * Math.sin(circle.angle)) * (circle.radius);
                    //Draw the first ball with position x and y
                    context.fillStyle = "#000000";
                    context.beginPath();
                    context.arc(ball.x, ball.y, 10, 0, Math.PI * 2, true);
                    context.fill();
                    context.closePath();

                    //alert("male Positions "+"X:  "+ball.x+ " Y: "+ball.y);

                    // female
                    new_angle = div_angle * i + 4;
                    //Starting positions for ball 2 at different points on the ellipse
                    circle.angle = (new_angle * (0.0174532925)) + ball.speed;
                    //elliptical x position and y position for animation for the second ball
                    //ball.x and ball.y record the second balls positions
                    ball.x = circle.centerX - (a * Math.cos(circle.angle)) * (circle.radius);
                    ball.y = circle.centerY + (b * Math.sin(circle.angle)) * (circle.radius);
                    context.fillStyle = "#000000";
                    context.beginPath();
                    context.arc(ball.x, ball.y, 10, 0, Math.PI * 2, true);
                    context.fill();
                    context.closePath();

                    //alert("female Positions "+"X:  "+ball.x+ " Y: "+ball.y);

                    //Record the ball positions in elements array for locating positions with mouse coordinates.
                    elements[i] = {
                        id: i,
                        femaleX: ball.x,
                        femaleY: ball.y,
                        maleX: xx,
                        maleY: yy,
                        w: 10 //radius of the ball to draw while locating the positions
                    };
                    //Text Numbering
                    context.beginPath();
                    context.fillStyle = "blue";
                    context.font = "bold 16px Arial";
                    context.fillText(elements[i].id, ball.x - 20, ball.y + 20);
                    context.closePath();
                    // line drawing--Connecting lines to the balls from the center.
                    context.moveTo(mainX, mainY);
                    context.lineTo((ball.x + xx) / 2, (ball.y + yy) / 2);
                    //Draw line till the middle point between ball1 and ball2
                    context.stroke();
                    context.fill();
                    context.closePath();
                }
                // center point
                context.fillStyle = "#000000";
                context.beginPath();
                context.arc(mainX, mainY, 15, 0, Math.PI * 2, true);
                context.fill();
                context.closePath();

                context.restore();
            }

            // Event Listeners
            // Mouse move event to alert the position of the ball on screen


            document.getElementById("plus").addEventListener("click", function () {
                scaleFactor *= 1.1;
                drawScreen();
            }, false);

            document.getElementById("minus").addEventListener("click", function () {
                scaleFactor /= 1.1;
                drawScreen();
            }, false);

            // Event listeners to handle screen panning
            context.canvas.addEventListener("mousedown", function (evt) {
                mouseDown = true;
                startDragOffset.x = evt.clientX - translatePos.x;
                startDragOffset.y = evt.clientY - translatePos.y;
            });

            context.canvas.addEventListener("mouseup", function (evt) {
                mouseDown = false;
            });

            context.canvas.addEventListener("mouseover", function (evt) {
                mouseDown = false;
            });

            context.canvas.addEventListener("mouseout", function (evt) {
                mouseDown = false;
            });

            context.canvas.addEventListener("mousemove", function (evt) {
                if (mouseDown) {
                    translatePos.x = evt.clientX - startDragOffset.x;
                    translatePos.y = evt.clientY - startDragOffset.y;

                    panX = translatePos.x;
                    panY = translatePos.y;

                    drawScreen();
                }

                evt.preventDefault();
                evt.stopPropagation();

                var mouseX = parseInt(evt.clientX - offsetX);
                var mouseY = parseInt(evt.clientY - offsetY);

                var mouseXT = parseInt((mouseX - panX) / scaleFactor);
                var mouseYT = parseInt((mouseY - panY) / scaleFactor);

                status.innerHTML = mouseXT + " | " + mouseYT;

                for (var i = 1; i < elements.length; i++) {
                    var b = elements[i];
                    context.closePath();
                    context.beginPath();
                    context.arc(b.femaleX, b.femaleY, 10, 0, Math.PI * 2);
                    context.arc(b.maleX, b.maleY, 10, 0, Math.PI * 2);

                    if (context.isPointInPath(mouseXT, mouseYT)) {
                        theCanvas.style.cursor = 'pointer';
                        alert(b.id + " female.x: " + b.femaleX + " female.y: " + b.femaleY + " ball.x: " + ball.x + " ball.y: " + ball.y);
                        return;
                    } else theCanvas.style.cursor = 'default';
                    context.closePath();
                }

            });`

3
你的代码变得太大了。首先要做的是将其分解为小部分,以便您可以进行简单的单元测试。例如,将矩阵处理作为单独的部分并检查它是否正常工作。编写几个小绘图函数来简化(如drawBackground、drawBall等),并从您的代码中去除事件处理的相关性(例如您的mousemove代码长达60行!!)。使用经过测试的小建造块,很快就会发现问题所在。 - GameAlchemist
哈哈!我曾经陷入了一段时间的迷茫,试图让它整体运作起来。但我将其分解为基础部分,并最终解决了问题。感谢您的建议 :) 我真的不知道为什么以前没有想到这样做。 - BoreBoar
常见的陷阱是没有意识到复杂性已经达到了“临界”阈值,不要担心。当我遇到一个 bug 时,我总是想知道它是否只是一个 bug,还是需要重构我的代码的迹象。顺便说一下,我没有看到你发布的编辑有太多变化,为了给出一个重构的例子,我在这个 fiddle 中对 drawScreen() 进行了(部分)重构:http://jsfiddle.net/gamealchemist/A8hgz/2/。 - GameAlchemist
1个回答

17

使用变换矩阵在以下情况下是有用甚至必要的:

  • 如果您深度嵌套变换。
  • 如果您使用不同的变换来改变不同的图形。
  • 如果您需要中间转换坐标。
  • 如果您进行涉及扭曲的变换。
  • 如果您进行涉及旋转的变换。

但是对于简单情况下的整个画布平移和缩放,有一个更简单的方法。

首先,设置变量以保存当前缩放和平移的量:

var scaleFactor=1.00;
var panX=0;
var panY=0;

然后使用这些平移和缩放变量来进行所有的绘图。

  • 清空画布。
  • 保存未经转换的画布状态。
  • 使用 panX 变量进行平移。
  • 使用 scaleFactor 变量进行缩放。
  • 像在未变换的空间中一样绘制所有元素。
  • 将上下文还原为其未变换的状态。

示例代码:

function drawTranslated(){

    ctx.clearRect(0,0,cw,ch);

    ctx.save();
    ctx.translate(panX,panY);
    ctx.scale(scaleFactor,scaleFactor);

    ctx.beginPath();
    ctx.arc(circleX,circleY,15,0,Math.PI*2);
    ctx.closePath();
    ctx.fillStyle=randomColor();
    ctx.fill();

    ctx.restore();

}

关于鼠标坐标:

浏览器总是返回未转换坐标系的鼠标位置。您的绘图已在转换空间中完成。如果您想要知道鼠标在转换空间中的位置,可以像这样将未转换的鼠标坐标转换为转换后的坐标:

var mouseXTransformed = (mouseX-panX) / scaleFactor;
var mouseYTransformed = (mouseY-panY) / scaleFactor;

以下是示例代码和演示:http://jsfiddle.net/m1erickson/HwNp3/

<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
    body{ background-color: ivory; }
    #canvas{border:1px solid red;}
</style>
<script>
$(function(){

    var canvas=document.getElementById("canvas");
    var ctx=canvas.getContext("2d");
    var $canvas=$("#canvas");
    var canvasOffset=$canvas.offset();
    var offsetX=canvasOffset.left;
    var offsetY=canvasOffset.top;
    var scrollX=$canvas.scrollLeft();
    var scrollY=$canvas.scrollTop();
    var cw=canvas.width;
    var ch=canvas.height;

    var scaleFactor=1.00;
    var panX=0;
    var panY=0;

    var circleX=150;
    var circleY=150;

    var $screen=$("#screen");
    var $transformed=$("#transformed");
    var $trx=$("#trx");

    drawTranslated();

    $("#canvas").mousemove(function(e){handleMouseMove(e);});
    $("#scaledown").click(function(){ scaleFactor/=1.1; drawTranslated(); });
    $("#scaleup").click(function(){ scaleFactor*=1.1; drawTranslated(); });
    $("#panleft").click(function(){ panX-=10; drawTranslated(); });
    $("#panright").click(function(){ panX+=10; drawTranslated(); });


    function drawTranslated(){
        ctx.clearRect(0,0,cw,ch);

        ctx.save();
        ctx.translate(panX,panY);
        ctx.scale(scaleFactor,scaleFactor);

        ctx.beginPath();
        ctx.arc(circleX,circleY,15,0,Math.PI*2);
        ctx.closePath();
        ctx.fillStyle=randomColor();
        ctx.fill();

        ctx.restore();

        $trx.text("Pan: "+panX+", Scale: "+scaleFactor);
    }

    function handleMouseMove(e){
        e.preventDefault();
        e.stopPropagation();

        var mouseX=parseInt(e.clientX-offsetX);
        var mouseY=parseInt(e.clientY-offsetY);

        var mouseXT=parseInt((mouseX-panX)/scaleFactor);
        var mouseYT=parseInt((mouseY-panY)/scaleFactor);

        $screen.text("Screen Coordinates: "+mouseX+"/"+mouseY);

        $transformed.text("Transformed Coordinates: "+mouseXT+"/"+mouseYT);
    }

    function randomColor(){ 
        return('#'+Math.floor(Math.random()*16777215).toString(16));
    }

}); // end $(function(){});
</script>
</head>
<body>
    <h3>Transformed coordinates are mouseXY in transformed space.<br>The circles center is always at translated [150,150]</h3>
    <h4 id=screen>Screen Coordinates:</h4>
    <h4 id=transformed>Transformed Coordinates:</h4>
    <h4 id=trx>Pan & Scale</h4>
    <button id=scaledown>Scale Down</button>
    <button id=scaleup>Scale Up</button>
    <button id=panleft>Pan Left</button>
    <button id=panright>Pan Right</button><br>
    <canvas id="canvas" width=350 height=400></canvas>
</body>
</html>

非常感谢。解决了我的问题! - Hristo Enev

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接