如何在画布中缩放旋转的矩形

时间:2016-12-13 22:05:10

标签: javascript math canvas scale

当旋转形状时,用于通过角手柄缩放矩形的正确数学公式是什么?

更新:

问题更多的是关于手柄上的鼠标按下事件背后的数学以及形状的实际尺寸。当手柄在旋转的形状上移动时,用于计算形状位置和缩放尺寸的正确数学是什么?

我已经创建了一个项目的小提琴来展示一个例子: https://jsfiddle.net/8b5zLupf/38/

小提琴中画布上的灰色形状可以移动和缩放,但由于形状是旋转的,因此缩放形状时的数学运算以及在缩放计算不正确时保持形状位置。

项目将进行缩放,但不会通过相反的点锁定形状并均匀缩放。

我用来缩放带有方面的形状的代码区域如下:

resizeShapeWithAspect: function(currentHandle, shape, mouse)
{
    var self = this;
    var getModifyAspect = function(max, min, value)
    {
        var ratio = max / min;
        return value * ratio;
    };

    var modify = {
        width: 0,
        height: 10
    };
    var direction = null,
        objPos = shape.position,
        ratio = this.getAspect(shape.width, shape.height);
    switch (currentHandle)
    {
        case 'topleft':
            modify.width = shape.width + (objPos.x - mouse.x);
            modify.height = shape.height + (objPos.y - mouse.y);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeX = (modify.width - shape.width);
            var changeY = (modify.height - shape.height);
            objPos.x = mouse.x + changeX;
            objPos.y = mouse.y + changeY;
            break;
        case 'topright':
            modify.width = mouse.x - objPos.x;
            modify.height = shape.height + (objPos.y - mouse.y);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeY = (modify.height - shape.height);
            objPos.y = mouse.y + changeY;
            break;
        case 'bottomleft':
            modify.width = shape.width + (objPos.x - mouse.x);
            modify.height = mouse.y - objPos.y;

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeX = (modify.width - shape.width);
            objPos.x = mouse.x + changeX;
            break;
        case 'bottomright':
            modify.width = mouse.x - objPos.x;
            modify.height = mouse.y - objPos.y;

            this.scale(shape, modify);
            break;

        case 'top':
            var oldWidth = shape.width;
            modify.width = shape.width + (objPos.x + mouse.x);
            modify.height = shape.height + (objPos.y - mouse.y);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeX = ((shape.width - oldWidth) / 2);
            var changeY = (modify.height - shape.height);
            objPos.x -= changeX;
            objPos.y = mouse.y + changeY;
            break;

        case 'left':
            var oldHeight = shape.height;
            modify.width = shape.width + (objPos.x - mouse.x);
            modify.height = getModifyAspect(modify.width, shape.width, shape.height);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeX = (modify.width - shape.width);
            var changeY = ((shape.height - oldHeight) / 2);
            objPos.x = mouse.x + changeX;
            objPos.y -= changeY;
            break;

        case 'bottom':
            var oldWidth = shape.width;
            modify.height = mouse.y - objPos.y;
            modify.width = getModifyAspect(modify.height, shape.height, shape.width);

            this.scale(shape, modify);

            var changeX = ((shape.width - oldWidth) / 2);
            objPos.x -= changeX;
            break;

        case 'right':
            var oldHeight = shape.height;
            modify.width = mouse.x - objPos.x;
            modify.height = getModifyAspect(modify.width, shape.width, shape.height);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeY = ((shape.height - oldHeight) / 2);
            objPos.y -= changeY;
            break;
    }
}

我修改了上面的代码以使用角度,但它无法正常工作。

resizeShapeWithAspectAndRotate: function(currentHandle, shape, mouse)
{
    var self = this;
    var getModifyAspect = function(max, min, value)
    {
        var ratio = max / min;
        return value * ratio;
    };

    var modify = {
        width: 0,
        height: 10
    };
    var direction = null,
        objPos = shape.position,
        ratio = this.getAspect(shape.width, shape.height),
        handles = shape.getHandlePositions();
    switch (currentHandle)
    {
        case 'topleft':
            var handle = this.getHandleByLabel(handles, 'topleft');
            var opositeHandle = this.getOpositeHandle(handles, 'topleft');
            var distance = canvasMath.distance(handle, mouse);

            modify.width = shape.width + (distance);
            modify.height = shape.height + (distance);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeX = (modify.width - shape.width);
            var changeY = (modify.height - shape.height);
            //shape.position.x = mouse.x + changeX;
            //shape.position.y = mouse.y + changeY;
            break;
        case 'topright':
            modify.width = mouse.x - objPos.x;
            modify.height = shape.height + (objPos.y - mouse.y);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeY = (modify.height - shape.height);
            objPos.y = mouse.y + changeY;
            break;
        case 'bottomleft':
            modify.width = shape.width + (objPos.x - mouse.x);
            modify.height = mouse.y - objPos.y;

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeX = (modify.width - shape.width);
            objPos.x = mouse.x + changeX;
            break;
        case 'bottomright':
            modify.width = mouse.x - objPos.x;
            modify.height = mouse.y - objPos.y;

            this.scale(shape, modify);
            break;

        case 'top':
            var oldWidth = shape.width;
            modify.width = shape.width + (objPos.x + mouse.x);
            modify.height = shape.height + (objPos.y - mouse.y);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeX = ((shape.width - oldWidth) / 2);
            var changeY = (modify.height - shape.height);
            objPos.x -= changeX;
            objPos.y = mouse.y + changeY;
            break;

        case 'left':
            var oldHeight = shape.height;
            modify.width = shape.width + (objPos.x - mouse.x);
            modify.height = getModifyAspect(modify.width, shape.width, shape.height);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeX = (modify.width - shape.width);
            var changeY = ((shape.height - oldHeight) / 2);
            objPos.x = mouse.x + changeX;
            objPos.y -= changeY;
            break;

        case 'bottom':
            var oldWidth = shape.width;
            modify.height = mouse.y - objPos.y;
            modify.width = getModifyAspect(modify.height, shape.height, shape.width);

            this.scale(shape, modify);

            var changeX = ((shape.width - oldWidth) / 2);
            objPos.x -= changeX;
            break;

        case 'right':
            var oldHeight = shape.height;
            modify.width = mouse.x - objPos.x;
            modify.height = getModifyAspect(modify.width, shape.width, shape.height);

            this.scale(shape, modify);

            /* we need to setup the shape position by getting the
            offset from where the object would have been without the
            scale and add that to the position */
            var changeY = ((shape.height - oldHeight) / 2);
            objPos.y -= changeY;
            break;
    }
},

getHandleByLabel: function(handles, label)
{
    if (handles)
    {
        for (var i = 0, maxLength = handles.length; i < maxLength; i++)
        {
            var handle = handles[i];
            if (label === handle.label)
            {
                return handle;
            }
        }
    }
    return false;
},

getOpositeHandle: function(handles)
{
    var handleLabel = this.currentHandle;
    if (handleLabel && handles)
    {
        switch (handleLabel)
        {
            case 'topleft':
                return this.getHandleByLabel(handles, 'bottomright');
            case 'top':
                return this.getHandleByLabel(handles, 'bottom');
            case 'topright':
                return this.getHandleByLabel(handles, 'bottomleft');
            case 'right':
                return this.getHandleByLabel(handles, 'left');
            case 'bottomright':
                return this.getHandleByLabel(handles, 'topleft');
            case 'bottom':
                return this.getHandleByLabel(handles, 'top');
            case 'bottomleft':
                return this.getHandleByLabel(handles, 'topright');
            case 'left':
                return this.getHandleByLabel(handles, 'right');
        }
    }
    return false;
}

1 个答案:

答案 0 :(得分:3)

通过大量代码查找修复程序的方法,这里是设置缩放,平移和旋转的简单快捷方法

// scaleX, scaleY the two scales
// posX posY the position
// rotate the amount of rotation
ctx.setTransform(scaleX,0,0,scaleY,posX,posY);
ctx.rotate(rotate);

然后相对于原点绘制框(旋转点)

ctx.fillRect(-50,-50,100,100); /// box with center as origin
ctx.fillRect(0,0,100,100); /// box with top left as origin
ctx.fillRect(-100,-100,100,100); /// box with bottom right as origin

将变换恢复为画布默认

ctx.setTransform(1,0,0,1,0,0);

更新

转换,坐标和反转。

要操纵画布对象,可以使用变换矩阵。即将发布的规范可让您获得当前的转换并对其进行操作,但它仍处于实验阶段。现在你需要自己维护变换。

转换矩阵

变换矩阵由2个向量和一个坐标组成。这些矢量和坐标始终位于画布像素坐标中,表示像素x轴,y轴和原点位置的方向和长度。

ctx.setTransform的文档调用参数a, b, c, d, e, f,这些参数模糊了它们的实际上下文含义。我更喜欢称它们为xAx, xAy, yAx, yAy, ox, oy,其中xAx, xAy是X轴向量(x,y),yAx,yAy是Y轴向量(x,y)和ox,oy是原点(x, Y)。

因此对于像素为一个像素宽的默认变换,一个像素的高度并从画布的右上角开始

var xAx = 1;   // X axis vector
var xAy = 0;

var yAx = 0;   // Y axis vector
var yAy = 1;

var ox = 0;   // origin
var oy = 0;

可以用来设置默认转换(而不是使用保存和恢复)ctx.setTransform(xAx, xAy, yAx, yAy, ox, oy);

要使用矩阵进行平移,请将原点设置为所需的画布像素坐标。

ox = ctx.canvas.width / 2;   // centre the transformation
oy = ctx.canvas.height / 2;

要缩放,只需更改x轴或y轴的矢量长度。

var scaleX = 2;
var scaleY = 3;    

// scale x axis
xAx *= scaleX;
xAy *= scaleX;

// scale y axis
yAx *= scaleY;
yAy *= scaleY;

旋转有点棘手。现在我们将忽略任何偏斜并假设y轴总是0.5Pi弧度(从这里我将使用以Pi为单位的弧度.360deg是2R相当于(2 * Pi)弧度)或0.5R(90deg)来自从x轴顺时针方向。

要设置旋转,我们获得x轴的旋转单位矢量

var rotate = 1.0; // in Pi units radian

xAx = Math.cos(rotate * Math.PI); // get the rotated x axis
xAy = Math.sin(rotate * Math.PI);

yAx = Math.cos((rotate + 0.5) * Math.PI); // get the rotated y axis at 0.5R (90deg) clockwise from the x Axis
yAy = Math.sin((rotate + 0.5) * Math.PI);

我们可以利用所涉及的对称来稍微缩短等式(当你将100&#39s渲染为1000或者对象时很好)。要旋转矢量0.5R(90度),只需交换x和y分量,否定新的x分量。

// rotate a vector 0.5R (90deg)
var vx = 1;
var vy = 0;

var temp = vx;   // swap to rotate
vx = -vy;        // negate the new x
vy = temp;

// or use the ES6 destructuring syntax
[vx, vy] = [-vy, vx];  // easy as 

因此设置两轴的旋转

 // rotation now in radians
 rotate *= Math.PI; // covert from Pi unit radians to radians

 yAy = xAx = Math.cos(rotate);
 yAx = -(xAy = Math.sin(rotate));

 // shame the x of the y axis needs to be negated or ES6 syntax would be better in this case.
 [xAx, xAy] = [Math.cos(rotate), Math.sin(rotate)]; 
 [yAx, yAy] = [-xAy, xAx]; // negate the x for the y 

我们可以把所有这些放在一起,并从其分解的部分创建一个矩阵。

 // x, y the translation (the origin)
 // scaleX, scaleY the x and y scale,
 // r the rotation in radians 
 // returns the matrix as object
 function recomposeMatrix(x, y, scaleX, scaleY, rotate){
     var xAx,xAy,yAx,yAy;
     xAx = Math.cos(rotate);
     xAy = Math.sin(rotate);
     [yAx, yAy] = [-xAy * scaleY, xAx * scaleY];
     xAx *= scaleX;
     xAy *= scaleX;
     return {xAx, xAy, yAx, yAy, ox: x, oy :y};
 }

您可以将此矩阵移交给2D上下文进行渲染

var matrix =  recomposeMatrix(100,100,2,2,1);     
ctx.setTransform(matrix.xAx, matrix.xAy, matrix.yAx, matrix.yAy, matrix.ox, matrix.oy);

替代懒惰的程序员方式

 // x, y the translation (the origin)
 // scaleX, scaleY the x and y scale,
 // r the rotation in radians 
 // returns the matrix as array
 function recomposeMatrix(x, y, scaleX, scaleY, rotate){
     var yAx,yAy;
     yAx = -Math.sin(rotate);
     yAy = Math.cos(rotate);
     return [yAy * scaleX, - yAx * scaleX, yAx * scaleY, yAy * scaleY, x, y];
 }
 var matrix = recomposeMatrix(100,100,1,1,0);
 ctx.setTransform(...matrix);

转换点

现在你有了矩阵,你需要使用它。要用矩阵变换一个点,你可以使用矩阵数学(很多规则,等等等等)或者你使用矢量数学。

你有一个点x,y和带有两个轴向量和原点的矩阵。要旋转和缩放,您可以将矩阵x轴单独移动距离x,然后沿矩阵y轴移动距离y,最后添加原点。

 var px = 100;  // point to transform
 var py = 100;

 var matrix =  recomposeMatrix(100,100,2,2,1);  // get a matrix

 var tx,ty; // the transformed point

 // move along the x axis px units     
 tx = px * matrix.xAx;
 ty = px * matrix.xAy;
 // then along the y axis py units
 tx += py * matrix.yAx;
 ty += py * matrix.yAy;
 // then add the origin
 tx += matrix.ox;
 ty += matrix.oy;

作为一项功能

 function transformPoint(matrix,px,py){
     var x = px * matrix.xAx + py * matrix.yAx + matrix.ox;
     var y = px * matrix.xAy + py * matrix.yAy + matrix.oy;
     return {x,y};
 }

反转矩阵。

可能CG应用程序中的问题是相对于旋转的缩放对象定位一个点。我们需要得到一个分层和分离的坐标系(称为空间)的概念。

对于2D,这相对简单。您的屏幕或画布空间始终为像素,矩阵为[1,0,0,1,0,0]原点为0,0,x轴为1像素,顶部为y轴,y轴为1像素。< / p>

然后你会有世界空间。这是旋转,缩放和平移场景中所有对象的空间。然后你就拥有了每个对象的局部空间。这是对象自己单独的旋转,缩放和翻译。

对于答案简洁的情况,我将忽略屏幕和世界空间,但要说它们被组合起来以获得最终的本地空间。

因此,我们有一个旋转的,缩放的,已翻译的对象,您希望获得相对于对象的坐标,而不是屏幕空间坐标,但在本地它是自己的x和y轴。

要执行此操作,您需要对屏幕坐标(例如鼠标x,y)应用变换,以撤消将对象放置在其中的变换。您可以通过反转对象变换矩阵来获得该变换。

 // mat the matrix to transform.
var rMat = {}; // the inverted matrix.
var det =  mat.xAx * mat.yAy - mat.xAy * mat.yAx; // gets the scaling factor called determinate
rMat.xAx  = mat.yAy / det;
rMat.xAy  = -mat.xAy / det;
rMat.yAx  = -mat.yAx / det;
rMat.yAy  = mat.xAx / det;
// and invert the origin by moving it along the 90deg rotated axis inversely scaled
rMat.ox = (mat.yAx * mat.oy - mat.yAy * mat.ox) / det;
rMat.oy = -(mat.xAx * mat.oy - mat.xAy * mat.ox) / det;

作为一项功能

function invertMatrix(mat){
    var rMat = {}; // the inverted matrix.
    var det =  mat.xAx * mat.yAy - mat.xAy * mat.yAx; // gets the scaling factor called determinate
    rMat.xAx  = mat.yAy / det;
    rMat.xAy  = -mat.xAy / det;
    rMat.yAx  = -mat.yAx / det;
    rMat.yAy  = mat.xAx / det;
    // and invert the origin by moving it along the 90deg rotated axis inversely scaled
    rMat.ox = (mat.yAx * mat.oy - mat.yAy * mat.ox) / det;
    rMat.oy = -(mat.xAx * mat.oy - mat.xAy * mat.ox) / det;     
    return rMat;
}

全部放在一起

现在您可以获得所需的信息。

你有一个方框

var box = { x : -50, y : -50, w : 100, h : 100 };

你有一个位置刻度和旋转

var boxPos = {x : 100, y : 100, scaleX : 2, scaleY : 2, rotate : 1};

要渲染它,您需要创建一个变换,将上下文设置为该矩阵并渲染。

var matrix = recomposeMatrix(boxPos.x, boxPos.y, boxPos.scaleX, boxPos.scaleY, boxPos.rotate);
ctx.setTransform(matrix.xAx, matrix.xAy, matrix.yAx, matrix.yAy, matrix.ox, matrix.oy);
ctx.strokeRect(box.x, box.y, box.w, box.h);

要确定鼠标(在屏幕空间中)是否在框内,您希望鼠标位于本地(框坐标)。要做到这一点,你需要倒置的框矩阵,你应用于鼠标坐标。

var invMatrix = invertMatrix(matrix);
var mouseLocal = transformPoint(invMatrix, mouse.x, mouse.y);

if(mouseLocal.x > box.x && mouseLocal.x < box.x + box.W && mouseLocal.y > box.y && mouseLocal.y < box.y + box.h){
    // mouse is inside
}

就这么简单。 mouseLocal坐标位于框空间中,因此获取角点等的相对位置是简单的几何。

您可能认为获取鼠标相对坐标需要做很多工作。是的,也许是一个旋转的盒子。你可以使用绝对屏幕坐标。但是,如果旋转世界空间,然后盒子附加到另一个物体,另一个物体被缩放旋转和定位。世界的变换,obj1,obj2,最后你的盒子可以相乘,得到盒子的变换矩阵。反转一个矩阵并且您具有屏幕坐标的相对位置。

更多转换

你需要为转换矩阵提供一些额外的功能,所以编写自己的矩阵类是个好主意,或者你可以从github得到一个(或者很少写的很少),或者你可以使用大多数浏览器在实验阶段都具有内置矩阵支持,并且需要使用前缀或标志。在MDN中找到它们