如何在 canvas 中缩放旋转的矩形

How to scale a rotated rectangle in canvas

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

更新:

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

我用项目创建了一个 fiddle 来展示一个例子: https://jsfiddle.net/8b5zLupf/38/

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

该项目会缩放,但不会按相反的点锁定形状并统一缩放。

我用来缩放具有纵横比的形状的代码区域如下:

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

很多代码需要通过并找到修复方法,所以这是设置比例、平移和旋转的简单快速的方法

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

将转换恢复为 canvas 默认值

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

更新

变换、坐标和逆。

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

变换矩阵

变换矩阵由 2 个向量和一个坐标组成。这些向量和坐标总是在canvas像素坐标中,表示一个像素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).

因此对于默认变换,其中一个像素宽一个像素,高一个像素并且从 canvas

的右上角开始
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);

要使用矩阵进行平移,请将原点设置为您想要的 canvas 像素坐标。

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 到 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};
 }

反转矩阵。

5 月 CG 应用程序中的问题是定位相对于旋转缩放对象的点。我们需要了解坐标系(称为 spaces)分层和分离的概念。

对于 2D,这相对简单。你有屏幕或 Canvas space 总是以像素为单位矩阵是 [1,0,0,1,0,0] 原点在 0,0,x 轴顶部 1 个像素, y 轴向下 1 个像素。

那么你就会拥有世界space。这是旋转、缩放和平移场景中所有对象的 space。然后你有每个对象本地 space。这是对象自己独立的旋转、缩放和平移。

为了简洁起见,我将忽略 Screen 和 World space 但要说的是它们组合在一起以获得最终的本地 space.

所以我们有一个旋转、缩放、平移的对象,您想要获得相对于该对象的坐标,而不是屏幕 space 坐标,但它在本地有自己的 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);

要确定鼠标(在屏幕 space 中)是否在您希望鼠标位于本地(框坐标)的框内。为此,您需要应用到鼠标坐标的反转盒矩阵。

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 坐标在框space 中,因此它是简单的几何图形,可以得到相对于角落等的位置。

您可能认为获取鼠标相对坐标的工作量很大。是的,也许对于一个旋转的盒子来说是这样。您可以只使用绝对屏幕坐标。但是如果旋转世界 space,然后盒子附加到另一个对象,另一个对象被缩放旋转和定位。世界、obj1、obj2 和最后你的盒子的变换可以相乘以获得盒子的变换矩阵。反转那个矩阵,你就有了屏幕坐标的相对位置。

更多变换

您将需要一些额外的变换矩阵功能,因此最好编写您自己的矩阵 class,或者您可以从github,或者您可以使用大多数浏览器在实验阶段都具有的内置矩阵支持,并且需要设置前缀或标志才能使用。在 MDN

中找到它们