How to fix map boundaries on d3 cartographic raster reprojection? - javascript

I try to use the raster reprojection of a map following this example. If I change the example kavrayskiy7 projection by the Azimuthal Equidistant projection,
var projection = d3.geo.azimuthalEquidistant()
.scale(90)
.translate([width / 2, height / 2])
.clipAngle(180 - 1e-3)
.precision(.1);
it should project the Earth onto a disc (the image of the projection map). However, the raster reprojection goes beyond that disc and fills the entire canvas with an extended picture (the inverse projection function is not injective, several x/y points on the map correspond to a single lon/lat coordinates). In the original example, this should be avoided with the line
if (λ > 180 || λ < -180 || φ > 90 || φ < -90) { i += 4; continue; }
but for this example that does not work. I found other glitches for instance when using the Mollweide projection (two lines appear at the poles) due to the same efect.
To solve this, one way would be to fix the inverse projections so they return error or None when the x/y input is out of range. My attempt was to check if a point is on range using the forward projection of the whole sphere to obtain a SVG path with the boundary of the map, as given by this code:
var path = d3.geo.path()
.projection(projection);
var bdry = svg.append("defs").append("path")
.datum({type: "Sphere"})
.attr("id", "sphere")
.attr("d", path);
(see for instance this example). However, I found no easy method to check whether a point [x,y] is inside a SVG closed path.
So my questions are:
Is there a bug on the inverse projections, or am I not using them correctly?
How could I find if a [x,y] point is inside the svg path, assuming that this is the best approach?
By curiosity, where is the algorithm code of the d3 path function to obtain the boundary profile of the map? I could not find it on the github repo.
Thanks.
Edit: I went through all the 44 projections in this example and I found glitches on the following 25:
Albers, Bromley, Collignon, Eckert II, Eckert IV, Eckert VI, Hammer, Hill, Goode Homolosine, Lambert cylindrical equal-area, Larrivée, Laskowski, McBryde–Thomas Flat-Polar Parabolic, McBryde–Thomas Flat-Polar Quartic, McBryde–Thomas Flat-Polar Sinusoidal, Mollweide, Natural Earth, Nell–Hammer, Polyconic, Sinu-Mollweide, van der Grinten, van der Grinten IV, Wagner IV, Wagner VII, Winkel Tripel.

I'm using a second answer only because this is a different approach to the same problem. Again, this answer is an alternative approach that tries to avoid a point in polygon solution that uses an svg outline of the projection extent.
This alternative should (I've only tried a handful) work for any projection while my other answer works only for projections projected to a disc. Secondly, this approach doesn't attempt to define the projection area to determine if a pixel should be rendered, but uses d3.projection itself.
As multiple points can return the same value with projection.invert, we can run a forward projection to verify if a pixel should be drawn.
If projection(projection.invert(point)) == point then the point is within the bounds of our projection.
Granted, there may be some precision/rounding errors in this, so some degree of tolerance could be specified.
This check fits within the for loop:
for (var y = 0, i = -1; y < height; ++y) {
for (var x = 0; x < width; ++x) {
var p = projection.invert([x, y]), λ = p[0], φ = p[1];
var pxy = projection(p);
var tolerance = 0.5;
if ( λ > 180 || λ < -180 || φ > 90 || φ < -90 ) { i += 4; continue; }
if ( (Math.abs(pxy[0] - x) < tolerance ) && (Math.abs(pxy[1] - y) < tolerance ) ) {
var q = ((90 - φ) / 180 * dy | 0) * dx + ((180 + λ) / 360 * dx | 0) << 2;
targetData[++i] = sourceData[q];
targetData[++i] = sourceData[++q];
targetData[++i] = sourceData[++q];
targetData[++i] = 255;
}
else {
i += 4;
}
}
}
As with the other answer, I built a block out of it here.
I haven't checked this answer for performance, and it seems odd that this sort of check is needed, but it might be a suitable alternative approach to the svg approach proposed in your question.

While I'm pretty sure you are using the projection.inverse function correctly,
relying on :
if (λ > 180 || λ < -180 || φ > 90 || φ < -90) { i += 4; continue; }
to clip the projection will always fail as projection.inverse appears to always return angles within 180 degrees east/west. While there may be a way to modify the projection itself to return values greater than 180 degrees, it is likely more difficult than other approaches (And honestly, that goes well beyond any answer I can give).
Likewise, using an SVG path to represent the world outline and then using that as the basis to determine if a point should be drawn is probably complicating the matter.
Instead, assuming a circular disc, you can easily compute the radius of the disc and from there determine if a pixel should be drawn:
var edge = {};
var center = {};
edge.x = projection([180 - 1e-6, 0])[0];
edge.y = projection([180 - 1e-6, 0])[1];
center.x = width/2;
center.y = height/2;
var radius = Math.pow( Math.pow(center.x - edge.x,2) + Math.pow(center.y - edge.y,2) , 0.5 )
Using the radius of the disc, we can then compute if a pixel falls on the disc or beyond it in the for loop:
for (var y = 0, i = -1; y < height; ++y) {
for (var x = 0; x < width; ++x) {
var p = projection.invert([x, y]), λ = p[0], φ = p[1];
if (Math.pow( Math.pow(center.x-x,2) + Math.pow(center.y-y,2), 0.5) < radius) {
if ( λ > 180 || λ < -180 || φ > 90 || φ < -90 ) { i += 4; continue; }
var q = ((90 - φ) / 180 * dy | 0) * dx + ((180 + λ) / 360 * dx | 0) << 2;
targetData[++i] = sourceData[q];
targetData[++i] = sourceData[++q];
targetData[++i] = sourceData[++q];
targetData[++i] = 255;
}
else {
targetData[++i] = 0;
targetData[++i] = 0;
targetData[++i] = 0;
targetData[++i] = 0;
}
}
}
Together, these gave me:
For aesthetic effect, it might be worthwhile trimming the radius down by a certain percent. Of course for different projections this approach may be either difficult or impossible.
I've put the code into a bl.ock here (I moved it to d3 v4 in the process, partly to see if the behavior of projection.inverse is the same).
For the third part of your question, you could try d3's graticule (graticule.outline) function for some info on how d3 gets the boundary profile of a projection.

Related

Loop construction in Processing artistic rendition

The code is from here:
t=0
draw=_=>{t||createCanvas(W = 720,W)
t+=.01
B=blendMode
colorMode(HSB)
B(BLEND)
background(0,.1)
B(ADD)
for(y = 0; y < W; y += 7)
for(x = 0; x < W; x+=7)
dist(x, y, H = 360, H) +
!fill(x * y % 360, W, W,
T=tan(noise(x, y) * 9 + t))
< sin(atan2(y - H, x - H) * 2.5 + 84)**8 * 200 + 130?
circle(x, y + 30, 4/T) : 0}
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
I see that t is increased by 0.01 each iteration, but I am unsure whether for each t value the entire canvas is refreshed, or whether there is an endpoint somewhat set by :0}. Is this correct?
It also seems like there is a Boolean call in the middle basically comparing two distances to determine which circles are filled and how. If the < was instead > the circles would form a complementary pattern on the rendition.
The ! is explained here, as a way of saving space and calling a function as soon as it is declared. I presume it determines how points are filled with a certain variable color scheme depending on the Boolean operation result. The +!fill() is unfamiliar, as well as the ? at the end, and I guess that they amount to: if the x, y location is within the boundary of the star the circles are colored (filled) as such, but the negation in '!' is confusing.
Can I get an English explanation of the main structural points on this code (the loop and the Boolean) to match the syntax?
I have so far gathered that the basic loop is
for(y from 0 to the width of the canvas at increments of 7)
for(x from... )
check if the distance from (x , y) to 360 is less than sin()^8 * 200 + 130
If so fill (or not fill with the ! ????) with these colors
otherwise do nothing :0
This is what it might look like if it were written normally
let t = 0;
const W = 720;
// https://p5js.org/reference/#/p5/draw
// `draw` needs to be in the global scope so p5 can use it
draw = () => {
// create a canvas if this is the first frame
if (!t) createCanvas(W, W);
t += 0.01;
// Use HSB and blending to do the fancy effects
// The black circles are because we're ignoring stroke and so using its defaults
// The blending will eventually hide it anyway
colorMode(HSB);
blendMode(BLEND);
background(0, 0.1);
blendMode(ADD);
// iterate over 7px grid
for(y = 0; y < W; y += 7) {
for(x = 0; x < W; x += 7) {
// center
const H = 360;
// distance from center
const D = dist(x, y, H, H);
// pick an alpha based on location and time
const T = tan(noise(x, y) * 9 + t);
// set fill color
fill(x * y % 360, W, W, T);
// magic to calculate the star's boundary
// sine wave in polar coords, I think
const S = sin(atan2(y - H, x - H) * 2.5 + 84)**8 * 200 + 130;
// draw a circle if we're within the star's area
// circle's color, alpha, and radius depend on location and time
if (D < S) circle(x, y + 30, 4/T);
}
}
};
<script src="https://cdnjs.cloudflare.com/ajax/libs/p5.js/1.5.0/p5.js"></script>
Note that there are some hacks in the original code that are solely to help make it a one-liner or to reduce the number of characters. They don't have any meaningful effect on the results.
The main issue is the +! with ? and :0, which is terribly confusing, but clearly it is equivalent to
t=0
draw=_=>{t||createCanvas(W = 720,W)
t+=.01
B=blendMode
colorMode(HSB)
B(BLEND)
background(0,.1)
B(ADD)
for(y = 0; y < W; y += 7)
for(x = 0; x < W; x+=7)
if(dist(x, y, H = 360, H) < sin(atan2(y - H, x - H) * 2.5 + 84)**8 * 200 + 130){
fill(x * y % 360, W, W,
T=tan(noise(x, y) * 9 + t))
circle(x, y + 30, 4/T)}else{0}}
The Boolean in the ? applies to the dist() part (in absolute values the angle has to be less than sin()... + 130.
The + part I still don't understand, and hopefully will be addressed by someone who knows Processing or JS (not me). However, it probably forces the execution to identify and throw out with regards to filling (hence !) values that are too low for the sin(atan2()), which will happen at around 0 and pi.
Because of arctan2() being (here)
the number of spikes in the star will be a multiple of 5 going from 0 to 2 pi. The fact that changing the code to < sin(2 * atan2(...)... doubles the spikes implies that the fill() part is also in Boolean operation of its own.
The result of the Boolean determines whether the colorful fill is applied or not (applied if less than).
The :0 at the end is the else do nothing.

SAT Polygon Circle Collision - resolve the intersection in the direction of velocity & determine side of collision

Summary
This question is in JavaScript, but an answer in any language, pseudo-code, or just the maths would be great!
I have been trying to implement the Separating-Axis-Theorem to accomplish the following:
Detecting an intersection between a convex polygon and a circle.
Finding out a translation that can be applied to the circle to resolve the intersection, so that the circle is barely touching the polygon but no longer inside.
Determining the axis of the collision (details at the end of the question).
I have successfully completed the first bullet point and you can see my javascript code at the end of the question. I am having difficulties with the other parts.
Resolving the intersection
There are plenty of examples online on how to resolve the intersection in the direction with the smallest/shortest overlap of the circle. You can see in my code at the end that I already have this calculated.
However this does not suit my needs. I must resolve the collision in the opposite direction of the circle's trajectory (assume I already have the circle's trajectory and would like to pass it into my function as a unit-vector or angle, whichever suits).
You can see the difference between the shortest resolution and the intended resolution in the below image:
How can I calculate the minimum translation vector for resolving the intersection inside my test_CIRCLE_POLY function, but that is to be applied in a specific direction, the opposite of the circle's trajectory?
My ideas/attempts:
My first idea was to add an additional axis to the axes that must be tested in the SAT algorithm, and this axis would be perpendicular to the circle's trajectory. I would then resolve based on the overlap when projecting onto this axis. This would sort of work, but would resolve way to far in most situations. It won't result in the minimum translation. So this won't be satisfactory.
My second idea was to continue to use magnitude of the shortest overlap, but change the direction to be the opposite of the circle's trajectory. This looks promising, but there are probably many edge-cases that I haven't accounted for. Maybe this is a nice place to start.
Determining side/axis of collision
I've figured out a way to determine which sides of the polygon the circle is colliding with. For each tested axis of the polygon, I would simply check for overlap. If there is overlap, that side is colliding.
This solution will not be acceptable once again, as I would like to figure out only one side depending on the circle's trajectory.
My intended solution would tell me, in the example image below, that axis A is the axis of collision, and not axis B. This is because once the intersection is resolved, axis A is the axis corresponding to the side of the polygon that is just barely touching the circle.
My ideas/attempts:
Currently I assume the axis of collision is that perpendicular to the MTV (minimum translation vector). This is currently incorrect, but should be the correct axis once I've updated the intersection resolution process in the first half of the question. So that part should be tackled first.
Alternatively I've considered creating a line from the circle's previous position and their current position + radius, and checking which sides intersect with this line. However, there's still ambiguity, because on occasion there will be more than one side intersecting with the line.
My code so far
function test_CIRCLE_POLY(circle, poly, circleTrajectory) {
// circleTrajectory is currently not being used
let axesToTest = [];
let shortestOverlap = +Infinity;
let shortestOverlapAxis;
// Figure out polygon axes that must be checked
for (let i = 0; i < poly.vertices.length; i++) {
let vertex1 = poly.vertices[i];
let vertex2 = poly.vertices[i + 1] || poly.vertices[0]; // neighbouring vertex
let axis = vertex1.sub(vertex2).perp_norm();
axesToTest.push(axis);
}
// Figure out circle axis that must be checked
let closestVertex;
let closestVertexDistSqr = +Infinity;
for (let vertex of poly.vertices) {
let distSqr = circle.center.sub(vertex).magSqr();
if (distSqr < closestVertexDistSqr) {
closestVertexDistSqr = distSqr;
closestVertex = vertex;
}
}
let axis = closestVertex.sub(circle.center).norm();
axesToTest.push(axis);
// Test for overlap
for (let axis of axesToTest) {
let circleProj = proj_CIRCLE(circle, axis);
let polyProj = proj_POLY(poly, axis);
let overlap = getLineOverlap(circleProj.min, circleProj.max, polyProj.min, polyProj.max);
if (overlap === 0) {
// guaranteed no intersection
return { intersecting: false };
}
if (Math.abs(overlap) < Math.abs(shortestOverlap)) {
shortestOverlap = overlap;
shortestOverlapAxis = axis;
}
}
return {
intersecting: true,
resolutionVector: shortestOverlapAxis.mul(-shortestOverlap),
// this resolution vector is not satisfactory, I need the shortest resolution with a given direction, which would be an angle passed into this function from the trajectory of the circle
collisionAxis: shortestOverlapAxis.perp(),
// this axis is incorrect, I need the axis to be based on the trajectory of the circle which I would pass into this function as an angle
};
}
function proj_POLY(poly, axis) {
let min = +Infinity;
let max = -Infinity;
for (let vertex of poly.vertices) {
let proj = vertex.projNorm_mag(axis);
min = Math.min(proj, min);
max = Math.max(proj, max);
}
return { min, max };
}
function proj_CIRCLE(circle, axis) {
let proj = circle.center.projNorm_mag(axis);
let min = proj - circle.radius;
let max = proj + circle.radius;
return { min, max };
}
// Check for overlap of two 1 dimensional lines
function getLineOverlap(min1, max1, min2, max2) {
let min = Math.max(min1, min2);
let max = Math.min(max1, max2);
// if negative, no overlap
let result = Math.max(max - min, 0);
// add positive/negative sign depending on direction of overlap
return result * ((min1 < min2) ? 1 : -1);
};
I am assuming the polygon is convex and that the circle is moving along a straight line (at least for a some possibly small interval of time) and is not following some curved trajectory. If it is following a curved trajectory, then things get harder. In the case of curved trajectories, the basic ideas could be kept, but the actual point of collision (the point of collision resolution for the circle) might be harder to calculate. Still, I am outlining an idea, which could be extended to that case too. Plus, it could be adopted as a main approach for collision detection between a circle and a convex polygon.
I have not considered all possible cases, which may include special or extreme situations, but at least it gives you a direction to explore.
Transform in your mind the collision between the circle and the polygon into a collision between the center of the circle (a point) and a version of the polygon thickened by the circle's radius r, i.e. (i) each edge of the polygon is offset (translated) outwards by radius r along a vector perpendicular to it and pointing outside of the polygon, (ii) the vertices become circular arcs of radius r, centered at the polygons vertices and connecting the endpoints of the appropriate neighboring offset edges (basically, put circles of radius r at the vertices of the polygon and take their convex hull).
Now, the current position of the circle's center is C = [ C[0], C[1] ] and it has been moving along a straight line with direction vector V = [ V[0], V[1] ] pointing along the direction of motion (or if you prefer, think of V as the velocity of the circle at the moment when you have detected the collision). Then, there is an axis (or let's say a ray - a directed half-line) defined by the vector equation X = C - t * V, where t >= 0 (this axis is pointing to the past trajectory). Basically, this is the half-line that passes through the center point C and is aligned with/parallel to the vector V. Now, the point of resolution, i.e. the point where you want to move your circle to is the point where the axis X = C - t * V intersects the boundary of the thickened polygon.
So you have to check (1) first axis intersection for edges and then (2) axis intersection with circular arcs pertaining to the vertices of the original polygon.
Assume the polygon is given by an array of vertices P = [ P[0], P[1], ..., P[N], P[0] ] oriented counterclockwise.
(1) For each edge P[i-1]P[i] of the original polygon, relevant to your collision (these could be the two neighboring edges meeting at the vertex based on which the collision is detected, or it could be actually all edges in the case of the circle moving with very high speed and you have detected the collision very late, so that the actual collision did not even happen there, I leave this up to you, because you know better the details of your situation) do the following. You have as input data:
C = [ C[0], C[1] ]
V = [ V[0], V[1] ]
P[i-1] = [ P[i-1][0], P[i-1][1] ]
P[i] = [ P[i][0], P[i][1] ]
Do:
Normal = [ P[i-1][1] - P[i][1], P[i][0] - P[i-1][0] ];
Normal = Normal / sqrt((P[i-1][1] - P[i][1])^2 + ( P[i][0] - P[i-1][0] )^2);
// you may have calculated these already
Q_0[0] = P[i-1][0] + r*Normal[0];
Q_0[1] = P[i-1][1] + r*Normal[1];
Q_1[0] = P[i][0]+ r*Normal[0];
Q_1[1] = P[i][1]+ r*Normal[1];
Solve for s, t the linear system of equations (the equation for intersecting ):
Q_0[0] + s*(Q_1[0] - Q_0[0]) = C[0] - t*V[0];
Q_0[1] + s*(Q_1[1] - Q_0[1]) = C[1] - t*V[1];
if 0<= s <= 1 and t >= 0, you are done, and your point of resolution is
R[0] = C[0] - t*V[0];
R[1] = C[1] - t*V[1];
else
(2) For the each vertex P[i] relevant to your collision, do the following:
solve for t the quadratic equation (there is an explicit formula)
norm(P[i] - C + t*V )^2 = r^2
or expanded:
(V[0]^2 + V[1]^2) * t^2 + 2 * ( (P[i][0] - C[0])*V[0] + (P[i][1] - C[1])*V[1] )*t + ( P[i][0] - C[0])^2 + (P[i][1] - C[1])^2 ) - r^2 = 0
or if you prefer in a more code-like way:
a = V[0]^2 + V[1]^2;
b = (P[i][0] - C[0])*V[0] + (P[i][1] - C[1])*V[1];
c = (P[i][0] - C[0])^2 + (P[i][1] - C[1])^2 - r^2;
D = b^2 - a*c;
if D < 0 there is no collision with the vertex
i.e. no intersection between the line X = C - t*V
and the circle of radius r centered at P[i]
else
D = sqrt(D);
t1 = ( - b - D) / a;
t2 = ( - b + D) / a;
where t2 >= t1
Then your point of resolution is
R[0] = C[0] - t2*V[0];
R[1] = C[1] - t2*V[1];
Circle polygon intercept
If the ball is moving and if you can ensure that the ball always starts outside the polygon then the solution is rather simple.
We will call the ball and its movement the ball line. It starts at the ball's current location and end at the position the ball will be at the next frame.
To solve you find the nearest intercept to the start of the ball line.
There are two types of intercept.
Line segment (ball line) with Line segment (polygon edge)
Line segment (ball line) with circle (circle at each (convex only) polygon corner)
The example code has a Lines2 object that contains the two relevant intercept functions. The intercepts are returned as a Vec2containing two unit distances. The intercept functions are for lines (infinite length) not line sgements. If there is no intercept then the return is undefined.
For the line intercepts Line2.unitInterceptsLine(line, result = new Vec2()) the unit values (in result) are the unit distance along each line from the start. negative values are behind the start.
To take in account of the ball radius each polygon edge is offset the ball radius along its normal. It is important that the polygon edges have a consistent direction. In the example the normal is to the right of the line and the polygon points are in a clockwise direction.
For the line segment / circle intercepts Line2.unitInterceptsCircle(center, radius, result = new Vec2()) the unit values (in result) are the unit distance along the line where it intercepts the circle. result.x will always contain the closest intercept (assuming you start outside the circle). If there is an intercept there ways always be two, even if they are at the same point.
Example
The example contains all that is needed
The objects of interest are ball and poly
ball defines the ball and its movement. There is also code to draw it for the example
poly holds the points of the polygon. Converts the points to offset lines depending on the ball radius. It is optimized to that it only calculates the lines if the ball radius changes.
The function poly.movingBallIntercept is the function that does all the work. It take a ball object and an optional results vector.
It returns the position as a Vec2 of the ball if it contacts the polygon.
It does this by finding the smallest unit distance to the offset lines, and point (as circle) and uses that unit distance to position the result.
Note that if the ball is inside the polygon the intercepts with the corners is reversed. The function Line2.unitInterceptsCircle does provide 2 unit distance where the line enters and exits the circle. However you need to know if you are inside or outside to know which one to use. The example assumes you are outside the polygon.
Instructions
Move the mouse to change the balls path.
Click to set the balls starting position.
Math.EPSILON = 1e-6;
Math.isSmall = val => Math.abs(val) < Math.EPSILON;
Math.isUnit = u => !(u < 0 || u > 1);
Math.TAU = Math.PI * 2;
/* export {Vec2, Line2} */ // this should be a module
var temp;
function Vec2(x = 0, y = (temp = x, x === 0 ? (x = 0 , 0) : (x = x.x, temp.y))) {
this.x = x;
this.y = y;
}
Vec2.prototype = {
init(x, y = (temp = x, x = x.x, temp.y)) { this.x = x; this.y = y; return this }, // assumes x is a Vec2 if y is undefined
copy() { return new Vec2(this) },
equal(v) { return (this.x - v.x) === 0 && (this.y - v.y) === 0 },
isUnits() { return Math.isUnit(this.x) && Math.isUnit(this.y) },
add(v, res = this) { res.x = this.x + v.x; res.y = this.y + v.y; return res },
sub(v, res = this) { res.x = this.x - v.x; res.y = this.y - v.y; return res },
scale(val, res = this) { res.x = this.x * val; res.y = this.y * val; return res },
invScale(val, res = this) { res.x = this.x / val; res.y = this.y / val; return res },
dot(v) { return this.x * v.x + this.y * v.y },
uDot(v, div) { return (this.x * v.x + this.y * v.y) / div },
cross(v) { return this.x * v.y - this.y * v.x },
uCross(v, div) { return (this.x * v.y - this.y * v.x) / div },
get length() { return this.lengthSqr ** 0.5 },
set length(l) { this.scale(l / this.length) },
get lengthSqr() { return this.x * this.x + this.y * this.y },
rot90CW(res = this) {
const y = this.x;
res.x = -this.y;
res.y = y;
return res;
},
};
const wV1 = new Vec2(), wV2 = new Vec2(), wV3 = new Vec2(); // pre allocated work vectors used by Line2 functions
function Line2(p1 = new Vec2(), p2 = (temp = p1, p1 = p1.p1 ? p1.p1 : p1, temp.p2 ? temp.p2 : new Vec2())) {
this.p1 = p1;
this.p2 = p2;
}
Line2.prototype = {
init(p1, p2 = (temp = p1, p1 = p1.p1, temp.p2)) { this.p1.init(p1); this.p2.init(p2) },
copy() { return new Line2(this) },
asVec(res = new Vec2()) { return this.p2.sub(this.p1, res) },
unitDistOn(u, res = new Vec2()) { return this.p2.sub(this.p1, res).scale(u).add(this.p1) },
translate(vec, res = this) {
this.p1.add(vec, res.p1);
this.p2.add(vec, res.p2);
return res;
},
translateNormal(amount, res = this) {
this.asVec(wV1).rot90CW().length = -amount;
this.translate(wV1, res);
return res;
},
unitInterceptsLine(line, res = new Vec2()) { // segments
this.asVec(wV1);
line.asVec(wV2);
const c = wV1.cross(wV2);
if (Math.isSmall(c)) { return }
wV3.init(this.p1).sub(line.p1);
res.init(wV1.uCross(wV3, c), wV2.uCross(wV3, c));
return res;
},
unitInterceptsCircle(point, radius, res = new Vec2()) {
this.asVec(wV1);
var b = -2 * this.p1.sub(point, wV2).dot(wV1);
const c = 2 * wV1.lengthSqr;
const d = (b * b - 2 * c * (wV2.lengthSqr - radius * radius)) ** 0.5
if (isNaN(d)) { return }
return res.init((b - d) / c, (b + d) / c);
},
};
/* END of file */ // Vec2 and Line2 module
/* import {vec2, Line2} from "whateverfilename.jsm" */ // Should import vec2 and line2
const POLY_SCALE = 0.5;
const ball = {
pos: new Vec2(-150,0),
delta: new Vec2(10, 10),
radius: 20,
drawPath(ctx) {
ctx.beginPath();
ctx.arc(this.pos.x, this.pos.y, this.radius, 0, Math.TAU);
ctx.stroke();
},
}
const poly = {
bRadius: 0,
lines: [],
set ballRadius(radius) {
const len = this.points.length
this.bRadius = ball.radius;
i = 0;
while (i < len) {
let line = this.lines[i];
if (line) { line.init(this.points[i], this.points[(i + 1) % len]) }
else { line = new Line2(new Vec2(this.points[i]), new Vec2(this.points[(i + 1) % len])) }
this.lines[i++] = line.translateNormal(radius);
}
this.lines.length = i;
},
points: [
new Vec2(-200, -150).scale(POLY_SCALE),
new Vec2(200, -100).scale(POLY_SCALE),
new Vec2(100, 0).scale(POLY_SCALE),
new Vec2(200, 100).scale(POLY_SCALE),
new Vec2(-200, 75).scale(POLY_SCALE),
new Vec2(-150, -50).scale(POLY_SCALE),
],
drawBallLines(ctx) {
if (this.lines.length) {
const r = this.bRadius;
ctx.beginPath();
for (const l of this.lines) {
ctx.moveTo(l.p1.x, l.p1.y);
ctx.lineTo(l.p2.x, l.p2.y);
}
for (const p of this.points) {
ctx.moveTo(p.x + r, p.y);
ctx.arc(p.x, p.y, r, 0, Math.TAU);
}
ctx.stroke()
}
},
drawPath(ctx) {
ctx.beginPath();
for (const p of this.points) { ctx.lineTo(p.x, p.y) }
ctx.closePath();
ctx.stroke();
},
movingBallIntercept(ball, res = new Vec2()) {
if (this.bRadius !== ball.radius) { this.ballRadius = ball.radius }
var i = 0, nearest = Infinity, nearestGeom, units = new Vec2();
const ballT = new Line2(ball.pos, ball.pos.add(ball.delta, new Vec2()));
for (const p of this.points) {
const res = ballT.unitInterceptsCircle(p, ball.radius, units);
if (res && units.x < nearest && Math.isUnit(units.x)) { // assumes ball started outside poly so only need first point
nearest = units.x;
nearestGeom = ballT;
}
}
for (const line of this.lines) {
const res = line.unitInterceptsLine(ballT, units);
if (res && units.x < nearest && units.isUnits()) { // first unit.x is for unit dist on line
nearest = units.x;
nearestGeom = ballT;
}
}
if (nearestGeom) { return ballT.unitDistOn(nearest, res) }
return;
},
}
const ctx = canvas.getContext("2d");
var w = canvas.width, cw = w / 2;
var h = canvas.height, ch = h / 2
requestAnimationFrame(mainLoop);
// line and point for displaying mouse interaction. point holds the result if any
const line = new Line2(ball.pos, ball.pos.add(ball.delta, new Vec2())), point = new Vec2();
function mainLoop() {
ctx.setTransform(1,0,0,1,0,0); // reset transform
if(w !== innerWidth || h !== innerHeight){
cw = (w = canvas.width = innerWidth) / 2;
ch = (h = canvas.height = innerHeight) / 2;
}else{
ctx.clearRect(0,0,w,h);
}
ctx.setTransform(1,0,0,1,cw,ch); // center to canvas
if (mouse.button) { ball.pos.init(mouse.x - cw, mouse.y - ch) }
line.p2.init(mouse.x - cw, mouse.y - ch);
line.p2.sub(line.p1, ball.delta);
ctx.lineWidth = 1;
ctx.strokeStyle = "#000"
poly.drawPath(ctx)
ctx.strokeStyle = "#F804"
poly.drawBallLines(ctx);
ctx.strokeStyle = "#F00"
ctx.beginPath();
ctx.arc(ball.pos.x, ball.pos.y, ball.radius, 0, Math.TAU);
ctx.moveTo(line.p1.x, line.p1.y);
ctx.lineTo(line.p2.x, line.p2.y);
ctx.stroke();
ctx.strokeStyle = "#00f"
ctx.lineWidth = 2;
ctx.beginPath();
if (poly.movingBallIntercept(ball, point)) {
ctx.arc(point.x, point.y, ball.radius, 0, Math.TAU);
} else {
ctx.arc(line.p2.x, line.p2.y, ball.radius, 0, Math.TAU);
}
ctx.stroke();
requestAnimationFrame(mainLoop);
}
const mouse = {x:0, y:0, button: false};
function mouseEvents(e) {
const bounds = canvas.getBoundingClientRect();
mouse.x = e.pageX - bounds.left - scrollX;
mouse.y = e.pageY - bounds.top - scrollY;
mouse.button = e.type === "mousedown" ? true : e.type === "mouseup" ? false : mouse.button;
}
["mousedown","mouseup","mousemove"].forEach(name => document.addEventListener(name,mouseEvents));
#canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
Click to position ball. Move mouse to test trajectory
Vec2 and Line2
To make it easier a vector library will help. For the example I wrote a quick Vec2 and Line2 object (Note only functions used in the example have been tested, Note The object are designed for performance, inexperienced coders should avoid using these objects and opt for a more standard vector and line library)
It's probably not what you're looking for, but here's a way to do it (if you're not looking for perfect precision) :
You can try to approximate the position instead of calculating it.
The way you set up your code has a big advantage : You have the last position of the circle before the collision. Thanks to that, you can just "iterate" through the trajectory and try to find a position that is closest to the intersection position.
I'll assume you already have a function that tells you if a circle is intersecting with the polygon.
Code (C++) :
// What we need :
Vector startPos; // Last position of the circle before the collision
Vector currentPos; // Current, unwanted position
Vector dir; // Direction (a unit vector) of the circle's velocity
float distance = compute_distance(startPos, currentPos); // The distance from startPos to currentPos.
Polygon polygon; // The polygon
Circle circle; // The circle.
unsigned int iterations_count = 10; // The number of iterations that will be done. The higher this number, the more precise the resolution.
// The algorithm :
float currentDistance = distance / 2.f; // We start at the half of the distance.
Circle temp_copy; // A copy of the real circle to "play" with.
for (int i = 0; i < iterations_count; ++i) {
temp_copy.pos = startPos + currentDistance * dir;
if (checkForCollision(temp_copy, polygon)) {
currentDistance -= currentDistance / 2.f; // We go towards startPos by the half of the current distance.
}
else {
currentDistance += currentDistance / 2.f; // We go towards currentPos by the half of the current distance.
}
}
// currentDistance now contains the distance between startPos and the intersection point
// And this is where you should place your circle :
Vector intersectionPoint = startPos + currentDistance * dir;
I haven't tested this code so I hope there's no big mistake in there. It's also not optimized and there are a few problems with this approach (the intersection point could end up inside the polygon) so it still needs to be improved but I think you get the idea.
The other (big, depending on what you're doing) problem with this is that it's an approximation and not a perfect answer.
Hope this helps !
I'm not sure if I understood the scenario correctly, but an efficient straight forward use case would be, to only use a square bounding box of your circle first, calculating intersection of that square with your polygone is extremely fast, much much faster, than using the circle. Once you detect an intersection of that square and the polygone, you need to think or to write which precision is mostly suitable for your scenarion. If you need a better precision, than at this state, you can go on as this from here:
From the 90° angle of your sqare intersection, you draw a 45° degree line until it touches your circle, at this point, where it touches, you draw a new square, but this time, the square is embedded into the circle, let it run now, until this new square intersects the polygon, once it intersects, you have a guaranteed circle intersection. Depending on your needed precision, you can simply play around like this.
I'm not sure what your next problem is from here? If it has to be only the inverse of the circles trajectory, than it simply must be that reverse, I'm really not sure what I'm missing here.

Get coords of the intersection of the line in the plane

I have a canvas with this params:
width = 400, height = 400
and have a line passing through the point cursor[x1,y1] at an angle Q (in degree)
I need get all coords of the intersection of the line in the plane and write it to array. Now i use this equation: y - y1 = k * (x - x1)
to check all point I use this code:
var rad = Q * Math.PI/180;
for (ctrY = 0; ctrY < 400; ctrY += 1) {
for (ctrX = 0; ctrX < 400; ctrX += 1) {
if ( (ctrY - cursor.y) ===
~~(Math.tan(rad) * (ctrX - cursor.x)) ) {
z.push([ctrX, ctrY]);
}
}
}
For example when 0 < Q < 90 and cursor[x1,y1] = [200,200] z.length = 0 and it's not correct.
Where i'm wrong? Maybe there is a more convenient algorithm?
P.S. Sorry for my english
Seems you need line rastering algorithm. Consider Bresenham algorithm.
You can also look at DDA algorithm
I imagine an algorithm like this. (I only consider the case when 0 < Q < 90). First I will want to calculate the points where the line will intersect the Ox and Oy axes, considering the origin (0,0) point the upper left corner and if we imagine that the negative x and y values are respectively to the left and to the top of this point. Let x2 and y2 be the values where the line will intersect Ox and Oy. We want to calculate these values. We now have a system with 2 unknown variables (x2 and y2): Math.tan(rad) = (y1 -y2)/x1 and Math.tan(rad) = y1/(x1-x2). We can deduct these equations by drawing the line on the coordinate system and analyzing a bit. If we solve the system of equations we find something like: x2 = (x1*y1 -x1 * x1 * Math.tan(rad)/(2 * y1-x1)) and y2= y1- x1 * Math.tan(rad) (These need to be verified, I haven't double checked my calculus). A linear equation can be defined by the formula y = a*x + b and in our case a = x2 and b = y2. We can then calculate the points like this:
for (xIdx = 0; xIdx < 400; xIdx += 1) {
var ctrX = xIdx;
var ctrY = x2 * ctrX + y2 //todo: replace with the respective calculated variables x2 and y2(we could also define two functions in js) and proper rounding
z.push([ctrX, ctrY]);
}
I'm not sure if I'm 100% accurate but I hope you understand my idea.

Calculate roll angle for Google Maps Street View

Preamble: there's an issue logged with the Google Maps API, requesting the ability to correct the roll angle of street view tiles to compensate for hills. I've come up with a client-side workaround involving some css sorcery on the tile container. Here's my rotate function:
rotate: function() {
var tilesLoaded = setInterval(function() {
var tiles = $('map-canvas').getElementsByTagName('img');
for (var i=0; i<tiles.length; i++) {
if (tiles[i].src.indexOf(maps.panorama.getPano()) > -1) {
if (typeof maps.panorama.getPhotographerPov != 'undefined') {
var pov = maps.panorama.getPhotographerPov(),
pitch = pov.pitch,
cameraHeading = pov.heading;
/**************************
// I need help with my logic here.
**************************/
var yaw = pov.heading - 90;
if (yaw < 0) yaw += 360;
var scale = ((Math.abs(maps.heading - yaw) / 90) - 1) * -1;
pitch = pov.pitch * scale;
tiles[i].parentNode.parentNode.style.transform = 'rotate(' + pitch + 'deg)';
clearInterval(tilesLoaded);
return;
}
}
}
}, 20);
}
A full (and more thoroughly commented) proof-of-concept is at this JSFiddle. Oddly, the horizon is just about perfectly level if I do no calculation at all on the example in the JSFiddle, but that result isn't consistent for every Lat/Lng. That's just a coincidence.
So, I need to calculate the roll at the client's heading, given the client heading, photographer's heading, and photographer's pitch. Assume the photographer is either facing uphill or downhill, and pov.pitch is superlative (at the min or max limit). How can I calculate the desired pitch facing the side at a certain degree?
Edit: I found an equation that seems to work pretty well. I updated the code and the fiddle. While it seems to be pretty close to the answer, my algorithm is linear. I believe the correct equation should be logarithmic, resulting in subtler adjustments closer to the camera heading and opposite, while to the camera's left and right adjustments are larger.
I found the answer I was looking for. The calculation involves spherical trigonometry, which I didn't even know existed before researching this issue. If anyone notices any problems, please comment. Or if you have a better solution than the one I found, feel free to add your answer and I'll probably accept it if it's more reliable or significantly more efficient than my own.
Anyway, if the tile canvas is a sphere, 0 pitch (horizon) is a plane, and camera pitch is another plane intersecting at the photographer, the two planes project a spherical lune onto the canvas. This lune can be used to calculate a spherical triangle where:
polar angle = Math.abs(camera pitch)
base = camera heading - client heading
one angle = 90° (for flat horizon)
With two angles and a side available, other properties of a spherical triangle can be calculated using the spherical law of sines. The entire triangle isn't needed -- only the side opposite the polar angle. Because this is math beyond my skills, I had to borrow the logic from this spherical triangle calculator. Special thanks to emfril!
The jsfiddle has been updated. My production roll getter has been updated as follows:
function $(what) { return document.getElementById(what); }
var maps = {
get roll() {
function acos(what) {
return (Math.abs(Math.abs(what) - 1) < 0.0000000001)
? Math.round(Math.acos(what)) : Math.acos(what);
}
function sin(what) { return Math.sin(what); }
function cos(what) { return Math.cos(what); }
function abs(what) { return Math.abs(what); }
function deg2rad(what) { return what * Math.PI / 180; }
function rad2deg(what) { return what * 180 / Math.PI; }
var roll=0;
if (typeof maps.panorama.getPhotographerPov() != 'undefined') {
var pov = maps.panorama.getPhotographerPov(),
clientHeading = maps.panorama.getPov().heading;
while (clientHeading < 0) clientHeading += 360;
while (clientHeading > 360) clientHeading -= 360;
// Spherical trigonometry method
a1 = deg2rad(abs(pov.pitch));
a2 = deg2rad(90);
yaw = deg2rad((pov.heading < 0 ? pov.heading + 360 : pov.heading) - clientHeading);
b1 = acos((cos(a1) * cos(a2)) + (sin(a1) * sin(a2) * cos(yaw)));
if (sin(a1) * sin(a2) * sin(b1) !== 0) {
roll = acos((cos(a1) - (cos(a2) * cos(b1))) / (sin(a2) * sin(b1)));
direction = pov.heading - clientHeading;
if (direction < 0) direction += 360;
if (pov.pitch < 0)
roll = (direction < 180) ? rad2deg(roll) * -1 : rad2deg(roll);
else
roll = (direction > 180) ? rad2deg(roll) * -1 : rad2deg(roll);
} else {
// Fall back to algebraic estimate to avoid divide-by-zero
var yaw = pov.heading - 90;
if (yaw < 0) yaw += 360;
var scale = ((abs(clientHeading - yaw) / 90) - 1) * -1;
roll = pov.pitch * scale;
if (abs(roll) > abs(pov.pitch)) {
var diff = (abs(roll) - abs(pov.pitch)) * 2;
roll = (roll < 0) ? roll + diff : roll - diff;
}
}
}
return roll;
}, // end maps.roll getter
// ... rest of maps object...
} // end maps{}
After rotating the panorama tile container, the container also needs to be expanded to hide the blank corners. I was originally using the 2D law of sines for this, but I found a more efficient shortcut. Thanks Mr. Tan!
function deg2rad(what) { return what * Math.PI / 180; }
function cos(what) { return Math.cos(deg2rad(what)); }
function sin(what) { return Math.sin(deg2rad(what)); }
var W = $('map-canvas').clientWidth,
H = $('map-canvas').clientHeight,
Rot = Math.abs(maps.originPitch);
// pixels per side
maps.growX = Math.round(((W * cos(Rot) + H * cos(90 - Rot)) - W) / 2);
maps.growY = Math.round(((W * sin(Rot) + H * sin(90 - Rot)) - H) / 2);
There will be no more edits to this answer, as I don't wish to have it converted to a community wiki answer yet. As updates occur to me, they will be applied to the fiddle.

Apply gravity between two or more objects in HTML5 Canvas

I was creating something like a 2d gravity simulator, just for fun, and noticed that I'm a complete idiot in terms of math. I just can't get the gravity to work.
I've tried following the instructions found here but it looks weird and when the distance reaches zero, it goes completely buggy. If I add 1 to the distance as recommended in the question, all objects go upper left. I've even tried not modifying gravity when distances reach zero, but this doesn't change the behavior.
Here's the algorithm I'm using to apply gravity:
var distX = obj1.x - obj2.x,
distY = obj1.y - obj2.y;
if (obj1 != obj2) {
if (distY != 0) {
obj1.vy += -(1 / (distY));
}
if (distX != 0) {
obj1.vx += -(1 / (distX));
}
}
I've tried using other algorithms too, but most of them don't care for the distance between objects.
Note that I want the gravity to affect distant objects less than closer objects.
Instead of solving any equations we could use an approximation. dv/dt = G*M*m/r^2, but for small t we could use the approximation Δv = (G*M*m/r^2)*Δt.
When the objects collide I have implemented perfectly inelastic collision (see Wikipedia). This prevents the distance between two objects from being to small and therefore the maximum force is limited.
I also moved the part of the code where the object's position is changed to a separate loop, so the forces calculated for obj1 and obj2 are equal in size.
Demo
function tick() {
allObjs.forEach(function (obj1) {
allObjs.forEach(function (obj2) {
var diffX = obj2.x - obj1.x,
var diffY = obj2.y - obj1.y;
var distSquare = diffX*diffX + diffY*diffY
var dist = Math.sqrt(distSquare);
if (obj1 != obj2) {
if (dist > obj1.w/2 + obj2.w/2) {
//If you add mass to the objects change to obj2.mass
//instead of 50
var totalForce = 50/distSquare;
obj1.vx += totalForce * diffX / dist;
obj1.vy += totalForce * diffY / dist;
} else {
//Collision has occurred
//If you add mass to the objects change to
//tempX = (obj1.mass*obj1.vx + obj2.mass*obj2.vx)/(obj1.mass+
//obj2.mass);
//tempY = (obj1.mass*obj1.vy + obj2.mass*obj2.vy)/(obj1.mass+
//obj2.mass);
var tempX = (obj1.vx + obj2.vx)/2;
var tempY = (obj1.vy + obj2.vy)/2;
obj1.vx = tempX; obj2.vx = tempX;
obj1.vy = tempY; obj2.vy = tempY;
}
}
});
});
allObjs.forEach(function (obj1) {
obj1.x += obj1.vx / 25;
obj1.y += obj1.vy / 25;
});
stage.update();
}
Try
var distX = obj1.x - obj2.x,
distY = obj1.y - obj2.y;
var rsq = distX *distX + distY * distY;
var r = Math.sqrt(rsq);
var F = 50 / rsq; // constant chosen to be pleasing
var rhat_x = distX / r;
var rhat_y = distY / r;
var Fx = F * rhat_x;
var Fy = F * rhat_y;
obj1.vx += -Fx;
obj1.vy += -Fy;
obj2.vx += Fx;
obj2.vy += Fy;
This is very basic, its not taking mass into account its using the simplest possible way of solving the equations you should really use something like 5th order Runga-Kutta w/ error correction. But it does use the formula for gravitational
F = - G m1 m2 / r^2
where G is the universal gravitational constant, m1 m2 are the two masses (I've all of these to 1!) r^2 is the square of the distance between the objects. The force is in the direction to the other object, let this be a unit vector rhat so the vector version of the force, using 1 for the constants
F = - ( 1 / r^2 ) rhat
The above gives reasonable results it you start out with
createPlanet(50, 200, 2, 0, 1);
createPlanet(400, 200, 2, 0, -1);
you have to take care that the two planets don't get too close or the acceleration goes off to infinity and the velocities get too big.
While playing around I tried
var distX = obj1.x - obj2.x,
distY = obj1.y - obj2.y;
var rsq = distX *distX + distY * distY; // square of the distance
var r = Math.sqrt(rsq);
var Fx = distX / r;
var Fy = distY / r;
obj1.vx += -Fx;
obj1.vy += -Fy;
obj2.vx += Fx;
obj2.vy += Fy;
which gives pleasing but physically incorrect results.
Newton's equations of motion F = ma need to be solved here. You are not doing anything like that in your code. No wonder it isn't matching your intuition.
It would help to understand the physics.
This is a vector equation. The force is gravity, which follows an inverse distance squared law.
You also know how acceleration, velocity, and displacement are related. You have to know calculus.
For your 2D world, that means six equations for each body in the problem. Two bodies means 12 coupled equations.
Solving these equations means integrating all those coupled ordinary differential equations in time. You'll need to know something about numerical methods (e.g. Runga-Kutta 5th order integration w/ error correction).
You'd have a lot to learn to write such a thing yourself. I'd recommend looking into a JavaScript physics library like Box2D or something else that Google might find.

Categories

Resources