[英]How to scale a rotated rectangle in canvas
旋轉形狀時,通過拐角手柄縮放矩形的正確數學公式是什么?
更新:
問題更多是關於手柄上的鼠標按下事件以及形狀的實際大小背后的數學運算。 當手柄在旋轉的形狀上移動時,用於計算形狀位置和縮放大小的適當數學運算是什么?
我用該項目創建了一個小提琴來顯示示例: 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為單位使用弧度。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};
}
可能是CG應用程序中的問題是相對於旋轉的縮放對象定位點。 我們需要獲得一個坐標系統(稱為空間)分層和分離的概念。
對於2D,這相對簡單。 屏幕或畫布空間始終以像素為單位,矩陣的原點[1,0,0,1,0,0]始於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
}
就如此容易。 mouseLocal坐標位於box空間中,因此它是簡單的幾何圖形,可獲取角等的相對位置。
您可能會認為要獲取鼠標相對坐標需要進行大量工作。 是的,也許對於單個旋轉的盒子而言。 您可以只使用絕對屏幕坐標。 但是,如果旋轉世界空間,然后將盒子附加到另一個對象以及另一個縮放和旋轉的對象上,該怎么辦? 可以將世界,obj1,obj2以及最終您的盒子的變換相乘在一起,以獲得盒子的變換矩陣。 反轉一個矩陣,您將獲得屏幕坐標的相對位置。
您將需要一些額外的功能來使用轉換矩陣,因此編寫自己的矩陣類是一個好主意,或者您可以從github獲得一個(或者不好的一個,因為編寫得很差),或者可以使用內置的大多數瀏覽器在試驗階段都具有矩陣支持功能,因此需要設置前綴或標志才能使用。 在MDN中找到它們
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.