Reputation: 1564
I started to build a widget that uses svg asset that is a soccer court. I was working with regular 2d rectangle so far and it went well. However i wanted to replace that asset with this one:
I started to prototype on how to calculate the ball position in this kind of svg and its not going well. I guess that what i need is some kind of conversion from regular 2d rectangle model to something else that would account trapeze figure.
Maybe someone could help with understanding how its done. Lets say i have following coords {x: 0.2, y: 0.2}
which means i have to put the ball in 20% of width of court and 20% of its height. How do i do in this example?
EDIT #1
I read an answer posted by MBo and i made effort to rewrite delphi code to JavaScript.I dont know delphi at all but i think it went well, however after trying code out i bumped onto couple of problems:
trapeze is reversed (shorter horizotal line on the bottom), i attempted to fix it but without success, after couple of tries i had this as i wanted but then 0.2, 0.2
coord showed up on the bottom instead of closer to the top.
i am not sure if the calculation works correctly in general, center coord seems odly gravitating towards bottom (at least it is my visual impresion)
I attempted to fix reversed trapeze problem by playing with YShift = Hg / 4;
but it causes variety of issues. Would like to know how this works exactly
From what i understand, this script works in a way that you specify longer horizontal line Wd
and height Hg
and this produces a trapeze for you, is that correct?
EDIT #2
I updated demo snippet, it seems to work in some way, the only problem currently i have is that if i specify
Wd = 600; // width of source
Hg = 200; // height of source
the actuall trapeze is smaller (has less width and height),
also in some weird way manipulating this line:
YShift = Hg / 4;
changes the actuall height of trapeze.
its just then difficult to implement, as if i have been given svg court with certain size i need to be able to provide the actuall size to the function so then coords calculations will be accurate.
Lets say that i will be given court where i know 4 corners and based on that i need to be able to calculate coords. This implementation from my demo snippet, doesnt o it unfortunately.
Anyone could help alter the code or provide better implementation?
EDIT #3 - Resolution
this is final implementation:
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>
Upvotes: 3
Views: 663
Reputation: 17546
I have implemented it in plain HTML and JavaScript. You have to adjust the variables to your need. A and B are the lengths of small and large parallel sides and H is the height of trapeze. x0, y0 are the co-ordinates of left-bottom corner of the field. If it works out for you I will explain the math.
jQuery(function($){
var $field2d = $('.field2d'), $ball = $('.ball');
$field2d.on('mousemove', function(e){
var pos = translateBallPosition(e.offsetX, e.offsetY);
$ball.css({left: pos.x, top: pos.y});
});
var FB = {x0: 50, y0: 215, B: 640, A: 391, H: 158, P: 0};
FB.Wd = $field2d.width();
FB.Ht = $field2d.height();
FB.P = FB.B * FB.H / (FB.B - FB.A);
function translateBallPosition(X, Y){
var x = X / FB.Wd * FB.B, y = (FB.Ht - Y) / FB.Ht * FB.H;
y = y * FB.B * FB.H / (FB.A * FB.H + y * (FB.B - FB.A));
x = x / FB.P * (FB.P - y) + y * FB.B / FB.P / 2;
return {x: FB.x0 + x, y: FB.y0 - y};
}
});
.field2d {
position: relative;
border: 1px dashed gray;
background: #b0fdb5;
width: 400px;
height: 200px;
margin: 5px auto;
cursor: crosshair;
text-align: center;
}
.field3d {
position: relative;
width: 743px;
margin: auto;
}
.field3d>img {
width: 100%;
height: auto;
}
.ball {
position: absolute;
top: 0;
left: 0;
height: 20px;
width: 20px;
background: red;
border-radius: 10px;
margin: -20px 0 0 -10px;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="field3d">
<img src="https://i.sstatic.net/ciekU.png" />
<div class="ball"></div>
</div>
<div class="field2d">
Hover over this div to see corresponding ball position
</div>
Upvotes: 0
Reputation: 6247
You are looking for a projective mapping from (x,y)
in the court plane to (u,v)
in the screen plane. A projective mapping works like this:
(x,y,1)
M
from the left to get homogenous coordinates (u',v',l)
of the screen pixel(u,v) = (u'/l, v'/l)
The appropriate matrix M
can be computed from solving the cooresponding equations for e.g. the corners. Choosing the court center to coincide with origin and the the x
-axis pointing along the longer side and measuring the corner coordinates from your image, we get the following corresponding coordinates for a standard 105x68 court:
(-52.5, 34) -> (174, 57)
( 52.5, 34) -> (566, 57)
( 52.5,-34) -> (690,214)
(-52.5,-34) -> ( 50,214)
Setting up the equations for a projective mapping with matrix
/ a b c \
M = ( d e f )
\ g h 1 /
leads to a linear system with 8 equations and 8 unknowns, since each point correspondence (x,y) -> (u,v)
contributes two equations:
x*a + y*b + 1*c + 0*d + 0*e + 0*f - (u*x)*g - (u*y)*h = u
0*a + 0*b + 0*c + x*d + y*e + 1*f - (v*x)*g - (v*y)*h = v
(You get these two equations by expanding M (x y 1)^T = (l*u l*v l*1)^T
into three equations and substituting the value for l
from the third equation into the first two equations.)
The solution for the 8 unknowns a,b,c,...,h
put into the matrix gives:
/ 4.63 2.61 370 \
M = ( 0 -1.35 -116.64 )
\ 0 0.00707 1 /
So given e.g. the court center as {x: 0.5, y: 0.5}
you must first transform it into the above described coordinate system to get (x,y) = (0,0)
. Then you must compute
/ x \ / 4.63 2.61 370 \ / 0 \ / 370 \
M ( y ) = ( 0 -1.35 -116.64 ) ( 0 ) = ( 116.64 )
\ 1 / \ 0 0.00707 1 / \ 1 / \ 1 /
By dehomogenizing you get the screen coordinates of the center as
(u,v) = (370/1, 116.64/1) ~= (370,117)
A JavaScript implementation could look like this:
// using library https://cdnjs.cloudflare.com/ajax/libs/mathjs/3.2.1/math.js
// 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 = [
[174., 57.],
[566., 57.],
[690.,214.],
[ 50.,214.]];
// 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))
Upvotes: 3
Reputation: 80187
To make specific pespective projection that has axial symmetry and maps rectangle to isosceles trapezoid, we can build simpler model as I described here.
Let we want to map rectangle with coordinates (0,0)-(SrcWdt, SrcHgt)
with axial line at SrcWdt/2
into region with axial line at DstWdt/2
and coordinates of right corners RBX, RBY, RTX, RTY
Here we need (partial) perspective transformation:
X' = DstXCenter + A * (X - XCenter) / (H * Y + 1)
Y' = (RBY + E * Y) / (H * Y + 1)
and we can calculate coefficients A, E, H
without solving of eight linear equation system using coordinates of two corners of trapezoid.
Here is demonstration with Delphi code which finds coefficients and calculates mapping of some points into new region (Y-axis down, so perspective sight is from upper edge):
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);
Upvotes: 1