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



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

更新:

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

我为该项目创建了一个小提琴来展示一个例子: 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;
}

通过大量代码并找到修复的方法,因此这是一种设置比例、平移和旋转的简单快捷的方法

// 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 单位的弧度.360 度是 2R 相当于 (2 * Pi) 弧度)或 0.5R (90 度)从 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 轴上,然后沿矩阵 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],顶部为 x 轴 1 个像素,y 轴向下为 1 个像素。

然后你就有了世界空间。这是旋转、缩放和平移场景中所有对象的空间。然后你让每个对象都有本地空间。这是物体本身独立的旋转、缩放和平移。

为了简洁起见,我将忽略屏幕和世界空间,但要说它们结合在一起以获得最终的局部空间。

所以我们有一个旋转、缩放、平移的对象,你想要获得相对于对象的坐标,而不是屏幕空间坐标,而是局部它自己的 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
}

就这么简单。鼠标局部坐标在盒子空间中,因此获得角等的相对位置是简单的几何形状。

您可能认为获取鼠标相对坐标需要做很多工作。是的,也许对于单个旋转的盒子来说就是这样。您可以只使用绝对屏幕坐标。但是,如果旋转世界空间,然后将盒子附加到另一个对象和另一个既缩放旋转又定位的对象怎么办?世界的变换,obj1,obj2,最后你的盒子可以相乘,得到盒子的转换矩阵。反转该矩阵,即可获得屏幕坐标的相对位置。

更多转换

您将需要一些额外的转换矩阵功能,因此最好编写自己的矩阵类,或者您可以从 github 获取一个(或糟糕的矩阵,因为许多矩阵类写得很差),或者您可以使用大多数浏览器在实验阶段具有的内置矩阵支持,并且需要设置前缀或标志才能使用。在 MDN 中找到它们

最新更新