javascript - 将常规二维矩形坐标转换为梯形
问题描述
我开始构建一个使用 svg 资产的小部件,它是一个足球场。到目前为止,我一直在使用常规的 2d 矩形,并且进展顺利。但是我想用这个替换那个资产:
我开始制作关于如何在这种 svg 中计算球位置的原型,但效果并不好。我想我需要的是从常规 2d 矩形模型到其他可以解释空中飞人图形的转换。
也许有人可以帮助了解它是如何完成的。假设我有以下坐标{x: 0.2, y: 0.2}
,这意味着我必须将球放在球场宽度的 20% 和高度的 20% 处。在这个例子中我该怎么做?
编辑#1
我阅读了 MBo 发布的答案,并努力将 delphi 代码重写为 JavaScript。我根本不知道 delphi,但我认为它进展顺利,但是在尝试代码后我遇到了几个问题:
空中飞人被反转(底部的水平线较短),我试图修复它但没有成功,经过几次尝试后,我得到了我想要的,但随后
0.2, 0.2
坐标出现在底部而不是靠近顶部。我不确定计算是否正常工作,中心坐标似乎奇怪地向底部倾斜(至少这是我的视觉印象)
我试图通过玩来解决反向飞人问题,
YShift = Hg / 4;
但它会导致各种问题。想知道这是如何工作的据我了解,这个脚本的工作方式是您指定更长的水平线
Wd
和高度Hg
,这会为您产生一个空中飞人,对吗?
编辑#2
我更新了演示片段,它似乎以某种方式工作,目前我唯一的问题是,如果我指定
Wd = 600; // width of source
Hg = 200; // height of source
实际的空中飞人更小(宽度和高度更小),
也以某种奇怪的方式操纵这条线:
YShift = Hg / 4;
改变空中飞人的实际高度。
就在那时很难实现,好像我已经获得了一定大小的 svg 法庭,我需要能够为函数提供实际大小,这样坐标计算才会准确。
可以说,我将在我知道 4 个角的地方获得法庭,并基于此我需要能够计算坐标。我的演示片段中的这个实现,不幸的是没有。
任何人都可以帮助更改代码或提供更好的实现吗?
编辑#3 - 分辨率
这是最终的实现:
var math = {
inv: function (M){
if(M.length !== M[0].length){return;}
var i=0, ii=0, j=0, dim=M.length, e=0, t=0;
var I = [], C = [];
for(i=0; i<dim; i+=1){
I[I.length]=[];
C[C.length]=[];
for(j=0; j<dim; j+=1){
if(i==j){ I[i][j] = 1; }
else{ I[i][j] = 0; }
C[i][j] = M[i][j];
}
}
for(i=0; i<dim; i+=1){
e = C[i][i];
if(e==0){
for(ii=i+1; ii<dim; ii+=1){
if(C[ii][i] != 0){
for(j=0; j<dim; j++){
e = C[i][j];
C[i][j] = C[ii][j];
C[ii][j] = e;
e = I[i][j];
I[i][j] = I[ii][j];
I[ii][j] = e;
}
break;
}
}
e = C[i][i];
if(e==0){return}
}
for(j=0; j<dim; j++){
C[i][j] = C[i][j]/e;
I[i][j] = I[i][j]/e;
}
for(ii=0; ii<dim; ii++){
if(ii==i){continue;}
e = C[ii][i];
for(j=0; j<dim; j++){
C[ii][j] -= e*C[i][j];
I[ii][j] -= e*I[i][j];
}
}
}
return I;
},
multiply: function(m1, m2) {
var temp = [];
for(var p = 0; p < m2.length; p++) {
temp[p] = [m2[p]];
}
m2 = temp;
var result = [];
for (var i = 0; i < m1.length; i++) {
result[i] = [];
for (var j = 0; j < m2[0].length; j++) {
var sum = 0;
for (var k = 0; k < m1[0].length; k++) {
sum += m1[i][k] * m2[k][j];
}
result[i][j] = sum;
}
}
return result;
}
};
// standard soccer court dimensions
var soccerCourtLength = 105; // [m]
var soccerCourtWidth = 68; // [m]
// soccer court corners in clockwise order with center = (0,0)
var courtCorners = [
[-soccerCourtLength/2., soccerCourtWidth/2.],
[ soccerCourtLength/2., soccerCourtWidth/2.],
[ soccerCourtLength/2.,-soccerCourtWidth/2.],
[-soccerCourtLength/2.,-soccerCourtWidth/2.]];
// screen corners in clockwise order (unit: pixel)
var screenCorners = [
[50., 150.],
[450., 150.],
[350., 50.],
[ 150., 50.]
];
// compute projective mapping M from court to screen
// / a b c \
// M = ( d e f )
// \ g h 1 /
// set up system of linear equations A X = B for X = [a,b,c,d,e,f,g,h]
var A = [];
var B = [];
var i;
for (i=0; i<4; ++i)
{
var cc = courtCorners[i];
var sc = screenCorners[i];
A.push([cc[0], cc[1], 1., 0., 0., 0., -sc[0]*cc[0], -sc[0]*cc[1]]);
A.push([0., 0., 0., cc[0], cc[1], 1., -sc[1]*cc[0], -sc[1]*cc[1]]);
B.push(sc[0]);
B.push(sc[1]);
}
var AInv = math.inv(A);
var X = math.multiply(AInv, B); // [a,b,c,d,e,f,g,h]
// generate matrix M of projective mapping from computed values
X.push(1);
M = [];
for (i=0; i<3; ++i)
M.push([X[3*i], X[3*i+1], X[3*i+2]]);
// given court point (array [x,y] of court coordinates): compute corresponding screen point
function calcScreenCoords(pSoccer) {
var ch = [pSoccer[0],pSoccer[1],1]; // homogenous coordinates
var sh = math.multiply(M, ch); // projective mapping to screen
return [sh[0]/sh[2], sh[1]/sh[2]]; // dehomogenize
}
function courtPercToCoords(xPerc, yPerc) {
return [(xPerc-0.5)*soccerCourtLength, (yPerc-0.5)*soccerCourtWidth];
}
var pScreen = calcScreenCoords(courtPercToCoords(0.2,0.2));
var hScreen = calcScreenCoords(courtPercToCoords(0.5,0.5));
// Custom code
document.querySelector('.trapezoid').setAttribute('d', `
M ${screenCorners[0][0]} ${screenCorners[0][1]}
L ${screenCorners[1][0]} ${screenCorners[1][1]}
L ${screenCorners[2][0]} ${screenCorners[2][1]}
L ${screenCorners[3][0]} ${screenCorners[3][1]}
Z
`);
document.querySelector('.point').setAttribute('cx', pScreen[0]);
document.querySelector('.point').setAttribute('cy', pScreen[1]);
document.querySelector('.half').setAttribute('cx', hScreen[0]);
document.querySelector('.half').setAttribute('cy', hScreen[1]);
document.querySelector('.map-pointer').setAttribute('style', 'left:' + (pScreen[0] - 15) + 'px;top:' + (pScreen[1] - 25) + 'px;');
document.querySelector('.helper.NW-SE').setAttribute('d', `M ${screenCorners[3][0]} ${screenCorners[3][1]} L ${screenCorners[1][0]} ${screenCorners[1][1]}`);
document.querySelector('.helper.SW-NE').setAttribute('d', `M ${screenCorners[0][0]} ${screenCorners[0][1]} L ${screenCorners[2][0]} ${screenCorners[2][1]}`);
body {
margin:0;
}
.container {
width:500px;
height:200px;
position:relative;
border:solid 1px #000;
}
.view {
background:#ccc;
width:100%;
height:100%;
}
.trapezoid {
fill:none;
stroke:#000;
}
.point {
stroke:none;
fill:red;
}
.half {
stroke:none;
fill:blue;
}
.helper {
fill:none;
stroke:#000;
}
.map-pointer {
background-image:url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxzdmcgaWQ9IkxheWVyXzEiIHN0eWxlPSJlbmFibGUtYmFja2dyb3VuZDpuZXcgMCAwIDUxMiA1MTI7IiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCA1MTIgNTEyIiB4bWw6c3BhY2U9InByZXNlcnZlIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIj48Zz48cGF0aCBkPSJNMjU1LjksNmMtMjEuNywwLTQzLjQsNS4zLTYyLjMsMTZjLTMzLjksMTkuMi01Ny45LDU1LjMtNjEuOSw5NC4xYy0zLjcsMzYuMSw4LjksNzEuOCwyMiwxMDUuNyAgIGMxNS4xLDM4LjksMTAyLjEsMjI4LjksMTAyLjEsMjI4LjlzODcuNi0xOTEuNCwxMDIuOC0yMzAuOWMxMy4xLTM0LjIsMjUuNy03MC4yLDIxLjItMTA2LjVjLTUuMi00Mi4xLTM0LjctNzkuOS03My42LTk2LjggICBDMjkwLjUsOS41LDI3My4yLDYsMjU1LjksNnogTTI1NS45LDE4OS44Yy0zMywwLTU5LjgtMjYuOC01OS44LTU5LjhzMjYuOC01OS44LDU5LjgtNTkuOFMzMTUuNyw5NywzMTUuNywxMzAgICBTMjg5LDE4OS44LDI1NS45LDE4OS44eiIvPjxwYXRoIGQ9Ik0yOTIuMiwzOTcuMWMtNC4xLDguOS03LjksMTcuMi0xMS40LDI0LjdjMzYuOCwzLjYsNjMuNiwxNS4yLDYzLjYsMjguOGMwLDE2LjYtMzkuNiwzMC04OC40LDMwICAgYy00OC44LDAtODguNC0xMy40LTg4LjQtMzBjMC0xMy42LDI2LjgtMjUuMiw2My41LTI4LjhjLTMuNS03LjQtNy40LTE1LjgtMTEuNC0yNC43Yy02MC4yLDYuMy0xMDQuNSwyNy45LTEwNC41LDUzLjUgICBjMCwzMC42LDYzLjEsNTUuNCwxNDAuOCw1NS40czE0MC44LTI0LjgsMTQwLjgtNTUuNEMzOTYuOCw0MjUsMzUyLjQsNDAzLjQsMjkyLjIsMzk3LjF6IiBpZD0iWE1MSURfMV8iLz48L2c+PC9zdmc+');
display:block;
width:32px;
height:32px;
background-repeat:no-repeat;
background-size:32px 32px;
position:absolute;
opacity:.3;
}
<div class="container">
<svg class="view">
<path class="trapezoid"></path>
<circle class="point" r="3"></circle>
<circle class="half" r="3"></circle>
<path class="helper NW-SE"></path>
<path class="helper SW-NE"></path>
</svg>
<span class="map-pointer"></span>
</div>
解决方案
为了制作具有轴对称性并将矩形映射到等腰梯形的特定透视投影,我们可以构建更简单的模型,如我在此处描述的那样。
让我们想用坐标映射(0,0)-(SrcWdt, SrcHgt)
矩形SrcWdt/2
进入轴线在DstWdt/2
和右角坐标的区域RBX, RBY, RTX, RTY
这里我们需要(部分)透视变换:
X' = DstXCenter + A * (X - XCenter) / (H * Y + 1)
Y' = (RBY + E * Y) / (H * Y + 1)
并且我们可以A, E, H
在不使用梯形两个角的坐标求解八线性方程组的情况下计算系数。
这是使用 Delphi 代码的演示,它找到系数并计算一些点到新区域的映射(Y 轴向下,因此透视图来自上边缘):
procedure CalcAxialSymPersp(SrcWdt, SrcHgt, DstWdt, RBX, RBY, RTX, RTY: Integer;
var A, H, E: Double);
begin
A := (2 * RBX - DstWdt) / SrcWdt;
H := (A * SrcWdt/ (2 * RTX - DstWdt) - 1) / SrcHgt;
E := (RTY * (H * SrcHgt + 1) - RBY) / SrcHgt;
end;
procedure PerspMap(SrcWdt, DstWdt, RBY: Integer; A, H, E: Double;
PSrc: TPoint; var PPersp: TPoint);
begin
PPersp.X := Round(DstWdt / 2 + A * (PSrc.X - SrcWdt/2) / (H * PSrc.Y + 1));
PPersp.Y := Round((RBY + E * PSrc.Y) / (H * PSrc.Y + 1));
end;
var
Wd, Hg, YShift: Integer;
A, H, E: Double;
Pts: array[0..3] of TPoint;
begin
//XPersp = XPCenter + A * (X - XCenter) / (H * Y + 1)
//YPersp = (YShift + E * Y) / (H * Y + 1)
Wd := Image1.Width;
Hg := Image1.Height;
YShift := Hg div 4;
CalcAxialSymPersp(Wd, Hg, Wd,
Wd * 9 div 10, YShift, Wd * 8 div 10, Hg * 3 div 4,
A, H, E);
//map 4 corners
PerspMap(Wd, Wd, YShift, A, H, E, Point(Wd, 0), Pts[0]);
PerspMap(Wd, Wd, YShift, A, H, E, Point(Wd, Hg), Pts[1]);
PerspMap(Wd, Wd, YShift, A, H, E, Point(0, Hg), Pts[2]);
PerspMap(Wd, Wd, YShift, A, H, E, Point(0, 0), Pts[3]);
//draw trapezoid
Image1.Canvas.Brush.Style := bsClear;
Image1.Canvas.Polygon(Pts);
//draw trapezoid diagonals
Image1.Canvas.Polygon(Slice(Pts, 3));
Image1.Canvas.Polygon([Pts[1], Pts[2], Pts[3]]);
//map and draw central point
PerspMap(Wd, Wd, YShift, A, H, E, Point(Wd div 2, Hg div 2), Pts[0]);
Image1.Canvas.Ellipse(Pts[0].X - 3, Pts[0].Y - 3, Pts[0].X + 4, Pts[0].Y + 4);
//map and draw point at (0.2,0.2)
PerspMap(Wd, Wd, YShift, A, H, E, Point(Wd * 2 div 10, Hg * 2 div 10), Pts[0]);
Image1.Canvas.Ellipse(Pts[0].X - 3, Pts[0].Y - 3, Pts[0].X + 4, Pts[0].Y + 4);
推荐阅读
- c++ - unordered_map 中的值正在改变自身
- javascript - 无法从 Firebase Firestore 获取字段
- java - 有什么方法可以通过检查正在运行的活动配置文件来动态加载@ImportResources?
- r - 我正在使用: R x64 ,并且无法运行我的代码,我不知道是什么问题
- node.js - 为什么在NodeJS中将过滤器应用于mongoose-paginate时结果为空?
- elasticsearch - 根据嵌套数组中的字段对结果进行排名
- xamarin.android - Visual Studio Mac 部署到 Android 模拟器失败
- python - 在 python 中合并 2 个链表不起作用。创建的第三个链接列表在执行后给了我一个 NULL 值/结果
- typescript - NestJS DTO 扩展了 PartialType 中断验证
- mongodb - Mongodb query starts with inside $in Query