【问题标题】:HTML Canvas, mouse position after scale and translateHTML Canvas,缩放和翻译后的鼠标位置
【发布时间】:2016-04-08 10:05:24
【问题描述】:

我在画布中实现了一个缩放功能,就像这个:Zoom in on a point (using scale and translate)

现在我需要计算鼠标相对于画布的位置,我首先尝试这样:

var rect = this._canvas.getBoundingClientRect();
var x = ((event.clientX  - rect.left) / (rect.right - rect.left) * this._canvas.width);
var y = ((event.clientY  - rect.top) / (rect.bottom  - rect.top) * this._canvas.height);

在我放大之前效果很好……我试着这样做:

var x = ((event.clientX  - rect.left) / (rect.right - rect.left) * this._canvas.width) - this._canvas.offsetLeft ;
var y = ((event.clientY  - rect.top) / (rect.bottom  - rect.top) * this._canvas.height) - offset.top this._canvas.offSetTop ;

有什么提示吗?还是我应该更好地使用 JS 库与画布元素进行交互?如果有,你有什么经验吗?

【问题讨论】:

    标签: canvas html5-canvas


    【解决方案1】:

    逆矩阵

    这个答案也包括旋转,因为比例是矩阵中旋转的一部分,你不能真正排除其中一个。但是您可以忽略旋转(将其设置为零),只需设置缩放和平移,它就会执行您想要的操作。

    逆变换。它基本上与标准的 2D 转换相反。它将要求您跟踪转换,以便您可以创建逆变换,如果您希望使用ctx.rotationctx.scalectx.translatectx.transform,这在复杂的转换中可能会出现问题。由于您的要求很简单,我创建了一个简单的函数来进行最小转换。

    以下将变换矩阵和逆变换创建为两个数组,称为矩阵和 invMatrix。参数是平移 x,y(在画布坐标中)、缩放和旋转。

    var matrix = [1,0,0,1,0,0];
    var invMatrix = [1,0,0,1];
    function createMatrix(x, y, scale, rotate){
        var m = matrix; // just to make it easier to type and read
        var im = invMatrix; // just to make it easier to type and read
    
        // create the rotation and scale parts of the matrix
        m[3] =   m[0] = Math.cos(rotate) * scale;
        m[2] = -(m[1] = Math.sin(rotate) * scale);
    
        // add the translation
        m[4] = x;
        m[5] = y;
    
        // calculate the inverse transformation
    
        // first get the cross product of x axis and y axis
        cross = m[0] * m[3] - m[1] * m[2];
    
        // now get the inverted axis
        im[0] =  m[3] / cross;
        im[1] = -m[1] / cross;
        im[2] = -m[2] / cross;
        im[3] =  m[0] / cross;
     }  
    

    使用功能

    使用该功能很简单。只需调用所需的位置、比例和旋转值即可。

    逆向应用

    要从像素空间(屏幕 x,y)获取世界坐标(转换后的坐标),您需要应用逆变换

    function toWorld(x,y){        
        var xx, yy, m, result;
        m = invMatrix;
        xx = x - matrix[4];     // remove the translation 
        yy = y - matrix[5];     // by subtracting the origin
        // return the point {x:?,y:?} by multiplying xx,yy by the inverse matrix
        return {
           x:   xx * m[0] + yy * m[2],
           y:   xx * m[1] + yy * m[3]
        }
    }
    

    所以如果你想要鼠标在世界空间中的位置

    var mouseWorldSpace = toWorld(mouse.x,mouse.y);  // get the world space coordinates of the mouse
    

    该函数会将屏幕空间中的任何坐标转换为世界空间中的正确坐标。

    设置 2D 上下文变换

    要使用转换,您可以直接设置 2D 上下文转换

    var m = matrix;
    ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
    

    演示

    还有一个演示来展示它的使用情况。很多额外的代码,但我相信你能找到你需要的部分。 Demo 通过使用createMatrix 旋转、缩放和平移来动画变换,然后使用toWorld 将鼠标坐标转换到世界空间。

    // the demo function
    
    var demo = function(){
        /** fullScreenCanvas.js begin **/
        // create a full document canvas on top 
        var canvas = (function(){
            var canvas = document.getElementById("canv");
            if(canvas !== null){
                document.body.removeChild(canvas);
            }
            // creates a blank image with 2d context
            canvas = document.createElement("canvas"); 
            canvas.id = "canv";    
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight; 
            canvas.style.position = "absolute";
            canvas.style.top = "0px";
            canvas.style.left = "0px";
            canvas.style.zIndex = 1000;
            canvas.ctx = canvas.getContext("2d"); 
            document.body.appendChild(canvas);
            return canvas;
        })();
        var ctx = canvas.ctx;
        
        /** fullScreenCanvas.js end **/
        /** MouseFull.js begin **/
        // get the mouse data . This is a generic mouse handler I use  so a little over kill for this example
        var canvasMouseCallBack = undefined;  // if needed
        var mouse = (function(){
            var mouse = {
                x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false,
                interfaceId : 0, buttonLastRaw : 0,  buttonRaw : 0,
                over : false,  // mouse is over the element
                bm : [1, 2, 4, 6, 5, 3], // masks for setting and clearing button raw bits;
                getInterfaceId : function () { return this.interfaceId++; }, // For UI functions
                startMouse:undefined,
            };
            function mouseMove(e) {
                var t = e.type, m = mouse;
                m.x = e.offsetX; m.y = e.offsetY;
                if (m.x === undefined) { m.x = e.clientX; m.y = e.clientY; }
                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 (canvasMouseCallBack) { canvasMouseCallBack(m.x, m.y); }
                e.preventDefault();
            }
            function startMouse(element){
                if(element === undefined){
                    element = document;
                }
                "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",").forEach(
                function(n){element.addEventListener(n, mouseMove);});
                element.addEventListener("contextmenu", function (e) {e.preventDefault();}, false);
            }
            mouse.mouseStart = startMouse;
            return mouse;
        })();
        if(typeof canvas === "undefined"){
            mouse.mouseStart();
        }else{
            mouse.mouseStart(canvas);
        }
        /** MouseFull.js end **/
        
        
        // some stuff to draw a grid
        var gridStart= -(canvas.width/10)*4;
        var gridEnd = (canvas.width/10)*14;
        var gridStepMajor = canvas.width/10;
        var gridStepMinor = canvas.width/20;
        var minorCol = "#999";
        var majorCol = "#000";
        var minorWidth = 1;
        var majorWidth = 3;
        
        // some stuf to animate the transformation
        var timer = 0;
        var timerStep = 0.01;
     
     
        //----------------------------------------------------------------------------
        // the code from the answer
        var matrix = [1, 0, 0, 1, 0, 0];      // normal matrix
        var invMatrix = [1, 0, 0, 1];   // inverse matrix
        function createMatrix(x, y, scale, rotate){
            var m = matrix; // just to make it easier to type and read
            var im = invMatrix; // just to make it easier to type and read
            // create the scale and rotation part of the matrix
            m[3] =   m[0] = Math.cos(rotate) * scale;
            m[2] = -(m[1] = Math.sin(rotate) * scale);
            // translation
            m[4] = x;
            m[5] = y;
            
            // calculate the inverse transformation
            // first get the cross product of x axis and y axis
            cross = m[0] * m[3] - m[1] * m[2];
            // now get the inverted axies
            im[0] =  m[3] / cross;
            im[1] = -m[1] / cross;
            im[2] = -m[2] / cross;
            im[3] =  m[0] / cross;
         }  
    
        // function to transform to world space
        function toWorld(x,y){
            var xx, yy, m;
            m = invMatrix;
            xx = x - matrix[4];     
            yy = y - matrix[5];     
            return {
               x:   xx * m[0] + yy * m[2] ,
               y:   xx * m[1] + yy * m[3]
            }
        }
        //----------------------------------------------------------------------------
    
    
        // center of canvas    
        var cw = canvas.width / 2;
        var ch = canvas.height / 2;
       
    
        // the main loop
        function update(){
            var i,x,y,s;
            ctx.setTransform(1, 0, 0, 1, 0, 0);  // reset the transform so we can clear
            ctx.clearRect(0, 0, canvas.width, canvas.height);  // clear the canvas
            
            
            // animate the transformation
            timer += timerStep;
            x = Math.cos(timer) * gridStepMajor * 5 + cw;  // position
            y = Math.sin(timer) * gridStepMajor * 5 + ch;   
            s = Math.sin(timer/1.2) + 1.5;            // scale
            
            
            //----------------------------------------------------------------------
            // create the matrix at x,y scale = s and rotation time/3
            createMatrix(x,y,s,timer/3);      
            
            // use the created matrix to set the transformation
            var m = matrix;
            ctx.setTransform(m[0], m[1], m[2], m[3], m[4], m[5]);
            //----------------------------------------------------------------------------
            
            
            
            //draw a grid
            ctx.lineWidth = 2;
            ctx.beginPath();
            ctx.strokeStyle = majorCol ;
            ctx.lineWidth = majorWidth;
            for(i = gridStart; i <= gridEnd; i+= gridStepMajor){
                ctx.moveTo(gridStart, i);
                ctx.lineTo(gridEnd, i);
                ctx.moveTo(i, gridStart);
                ctx.lineTo(i, gridEnd);
            }
            ctx.stroke();
            ctx.strokeStyle = minorCol ;
            ctx.lineWidth = minorWidth;
            for(i = gridStart+gridStepMinor; i < gridEnd; i+= gridStepMinor){
                ctx.moveTo(gridStart, i);
                ctx.lineTo(gridEnd, i);
                ctx.moveTo(i, gridStart);
                ctx.lineTo(i, gridEnd);
            }
            ctx.stroke();
            
            //---------------------------------------------------------------------
            // get the mouse world coordinates
            var mouseWorldPos = toWorld(mouse.x, mouse.y);
            //---------------------------------------------------------------------
            
            
            // marke the location with a cross and a circle;
            ctx.strokeStyle = "red";
            ctx.lineWidth = 3;
            ctx.beginPath();
            ctx.moveTo(mouseWorldPos.x - gridStepMajor, mouseWorldPos.y)
            ctx.lineTo(mouseWorldPos.x + gridStepMajor, mouseWorldPos.y)
            ctx.moveTo(mouseWorldPos.x, mouseWorldPos.y - gridStepMajor)
            ctx.lineTo(mouseWorldPos.x, mouseWorldPos.y + gridStepMajor)
            ctx.stroke();
            
            
            ctx.fillStyle = "red";
            ctx.strokeStyle = "yellow";
            ctx.lineWidth = 4;
            ctx.beginPath();
            ctx.arc(mouseWorldPos.x, mouseWorldPos.y, 6, 0, Math.PI*2);
            ctx.fill();
            ctx.stroke();
            ctx.fillStyle = "Blue";
            ctx.setTransform(1,0,0,1,0,0);
    
            ctx.font = "18px Arial";
            var str = "Mouse canvas X: "+ mouse.x + " Y: " +  mouse.y;
            ctx.fillText(str , 10 ,18);
            var str = "Mouse world X: "+ mouseWorldPos.x.toFixed(2) + " Y: " +  mouseWorldPos.y.toFixed(2);
            ctx.fillText(str , 10 ,36);
            
            
            // if not over request a new animtion frame
            if(!endItAll){
               requestAnimationFrame(update);
            }else{
                // if done remove the canvas
                var can = document.getElementById("canv");
                if(can !== null){
                    document.body.removeChild(can);
                }       
                // flag that we are ready to start again
                endItAll = false;
            }
        }
        update(); // start the animation
    }
    
    // Flag to indicate that the current execution should shut down
    var endItAll = false;
    // resizes but waits for the current running animnation to shut down 
    function resizeIt(){
        endItAll = true;
        function waitForIt(){
            if(!endItAll){
                demo();
            }else{
                setTimeout(waitForIt, 100);
            }
        }
        setTimeout(waitForIt, 100);
    }
    
    
    // starts the demo
    demo();
    // listen to resize events and resize canvas if needed
    window.addEventListener("resize",resizeIt)

    【讨论】:

      【解决方案2】:

      一步一步来:

      找到鼠标在画布上的坐标:

      var rect   =  canvas.getBoundingClientRect();
      var xMouse =  event.clientX  - rect.left;
      var yMouse =  event.clientY  - rect.top;
      

      标准化这些坐标,使它们在 [0;1] 中:

      var relX = xMouse / canvas.width;
      var relY = yMouse / canvas.height;
      

      现在说你的视图是由一个叫做...好吧...viewRect的矩形定义的,鼠标在视图中的位置是:

      var viewX = viewRect.left + relX*(viewRect.right-viewRect.left);
      var viewY = viewRect.top + relY*(viewRect.bottom-viewRect.top);
      

      当您启动您的应用程序时,您的矩形是 0,0,canvasWidth,canvasHeight。
      单击时,您必须调整矩形。

      如果点击意味着在 viewX、viewY 上放大zFactor,代码将如下所示:

      var newWidth = viewRect.width/zFactor;
      var newHeight = viewRect.height/zFactor;
      viewRect.left = viewX - newWidth/2;
      viewRect.right = viewX + newWidth/2;
      viewRect.top  = viewY - newHeight/2;
      viewRect.bottom = viewY + newHeight/2;
      

      你的绘图方法应该是这样的:

      context.save();
      context.translate((viewRect.left+viewRect.right )/ 2, ...) ;
      var scaleFactor = (viewRect.right+viewRect.left ) / canvasWidth;
      context.scale(scaleFactor, scaleFactor);
      
      ... draw
      
      context.restore();
      

      【讨论】:

        【解决方案3】:

        我没有跟踪各种变换,而是在画布上查询当前变换:

            function mouseUp(canvas, event) {
                const rect = canvas.getBoundingClientRect();
                const transform = graphics.getTransform();
                const canvasX = (event.clientX - rect.left - transform.e) / transform.a;
                const canvasY = (event.clientY - rect.top - transform.f) / transform.d;
        

        不处理歪斜,但它给出了我正在使用的方法的一般概念。

        【讨论】:

          猜你喜欢
          • 2020-09-07
          • 1970-01-01
          • 2022-01-10
          • 1970-01-01
          • 2020-05-28
          • 2022-08-16
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多