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.
I have an image which is a background containing a boxed area like this:
I know the exact positions of the corners of that shape, and I'd like to place another image within it. (So it appears to be inside the box).
I'm aware of the drawImage method for HTML5 canvas, but it seems to only support x, y, width, height parameters rather than exact coordinates. How might I draw an image onto a canvas at a specific set of coordinates, and ideally have the browser itself handle stretching the image.
Quadrilateral transform
One way to go about this is to use Quadrilateral transforms. They are different than 3D transforms and would allow you to draw to a canvas in case you want to export the result.
The example shown here is simplified and uses basic sub-divison and "cheats" on the rendering itself - that is, it draws in a small square instead of the shape of the sub-divided cell but because of the small size and the overlap we can get away with it in many non-extreme cases.
The proper way would be to split the shape into two triangles, then scan pixel wise in the destination bitmap, map the point from destination triangle to source triangle. If the position value was fractional you would use that to determine pixel interpolation (f.ex. bi-linear 2x2 or bi-cubic 4x4).
I do not intend to cover all this in this answer as it would quickly become out of scope for the SO format, but the method would probably be suitable in this case unless you need to animate it (it is not performant enough for that if you want high resolution).
Method
Lets start with an initial quadrilateral shape:
The first step is to interpolate the Y-positions on each bar C1-C4 and C2-C3. We're gonna need current position as well as next position. We'll use linear interpolation ("lerp") for this using a normalized value for t:
y1current = lerp( C1, C4, y / height)
y2current = lerp( C2, C3, y / height)
y1next = lerp(C1, C4, (y + step) / height)
y2next = lerp(C2, C3, (y + step) / height)
This gives us a new line between and along the outer vertical bars.
Next we need the X positions on that line, both current and next. This will give us four positions we will fill with current pixel, either as-is or interpolate it (not shown here):
p1 = lerp(y1current, y2current, x / width)
p2 = lerp(y1current, y2current, (x + step) / width)
p3 = lerp(y1next, y2next, (x + step) / width)
p4 = lerp(y1next, y2next, x / width)
x and y will be the position in the source image using integer values.
We can use this setup inside a loop that will iterate over each pixel in the source bitmap.
Demo
The demo can be found at the bottom of the answer. Move the circular handles around to transform and play with the step value to see its impact on performance and result.
The demo will have moire and other artifacts, but as mentioned earlier that would be a topic for another day.
Snapshot from demo:
Alternative methods
You can also use WebGL or Three.js to setup a 3D environment and render to canvas. Here is a link to the latter solution:
Three.js
and an example of how to use texture mapped surface:
Three.js texturing (instead of defining a cube, just define one place/face).
Using this approach will enable you to export the result to a canvas or an image as well, but for performance a GPU is required on the client.
If you don't need to export or manipulate the result I would suggest to use simple CSS 3D transform as shown in the other answers.
/* Quadrilateral Transform - (c) Ken Nilsen, CC3.0-Attr */
var img = new Image(); img.onload = go;
img.src = "https://i.imgur.com/EWoZkZm.jpg";
function go() {
var me = this,
stepEl = document.querySelector("input"),
stepTxt = document.querySelector("span"),
c = document.querySelector("canvas"),
ctx = c.getContext("2d"),
corners = [
{x: 100, y: 20}, // ul
{x: 520, y: 20}, // ur
{x: 520, y: 380}, // br
{x: 100, y: 380} // bl
],
radius = 10, cPoint, timer, // for mouse handling
step = 4; // resolution
update();
// render image to quad using current settings
function render() {
var p1, p2, p3, p4, y1c, y2c, y1n, y2n,
w = img.width - 1, // -1 to give room for the "next" points
h = img.height - 1;
ctx.clearRect(0, 0, c.width, c.height);
for(y = 0; y < h; y += step) {
for(x = 0; x < w; x += step) {
y1c = lerp(corners[0], corners[3], y / h);
y2c = lerp(corners[1], corners[2], y / h);
y1n = lerp(corners[0], corners[3], (y + step) / h);
y2n = lerp(corners[1], corners[2], (y + step) / h);
// corners of the new sub-divided cell p1 (ul) -> p2 (ur) -> p3 (br) -> p4 (bl)
p1 = lerp(y1c, y2c, x / w);
p2 = lerp(y1c, y2c, (x + step) / w);
p3 = lerp(y1n, y2n, (x + step) / w);
p4 = lerp(y1n, y2n, x / w);
ctx.drawImage(img, x, y, step, step, p1.x, p1.y, // get most coverage for w/h:
Math.ceil(Math.max(step, Math.abs(p2.x - p1.x), Math.abs(p4.x - p3.x))) + 1,
Math.ceil(Math.max(step, Math.abs(p1.y - p4.y), Math.abs(p2.y - p3.y))) + 1)
}
}
}
function lerp(p1, p2, t) {
return {
x: p1.x + (p2.x - p1.x) * t,
y: p1.y + (p2.y - p1.y) * t}
}
/* Stuff for demo: -----------------*/
function drawCorners() {
ctx.strokeStyle = "#09f";
ctx.lineWidth = 2;
ctx.beginPath();
// border
for(var i = 0, p; p = corners[i++];) ctx[i ? "lineTo" : "moveTo"](p.x, p.y);
ctx.closePath();
// circular handles
for(i = 0; p = corners[i++];) {
ctx.moveTo(p.x + radius, p.y);
ctx.arc(p.x, p.y, radius, 0, 6.28);
}
ctx.stroke()
}
function getXY(e) {
var r = c.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top}
}
function inCircle(p, pos) {
var dx = pos.x - p.x,
dy = pos.y - p.y;
return dx*dx + dy*dy <= radius * radius
}
// handle mouse
c.onmousedown = function(e) {
var pos = getXY(e);
for(var i = 0, p; p = corners[i++];) {if (inCircle(p, pos)) {cPoint = p; break}}
}
window.onmousemove = function(e) {
if (cPoint) {
var pos = getXY(e);
cPoint.x = pos.x; cPoint.y = pos.y;
cancelAnimationFrame(timer);
timer = requestAnimationFrame(update.bind(me))
}
}
window.onmouseup = function() {cPoint = null}
stepEl.oninput = function() {
stepTxt.innerHTML = (step = Math.pow(2, +this.value));
update();
}
function update() {render(); drawCorners()}
}
body {margin:20px;font:16px sans-serif}
canvas {border:1px solid #000;margin-top:10px}
<label>Step: <input type=range min=0 max=5 value=2></label><span>4</span><br>
<canvas width=620 height=400></canvas>
You can use CSS Transforms to make your image look like that box. For example:
img {
margin: 50px;
transform: perspective(500px) rotateY(20deg) rotateX(20deg);
}
<img src="https://via.placeholder.com/400x200">
Read more about CSS Transforms on MDN.
This solution relies on the browser performing the compositing. You put the image that you want warped in a separate element, overlaying the background using position: absolute.
Then use CSS transform property to apply any perspective transform to the overlay element.
To find the transform matrix you can use the answer from: How to match 3D perspective of real photo and object in CSS3 3D transforms
Let's say I need to put a text in the middle of the area of a triangle.
I can calculate the coordinates of the triangle's center using getBBox():
var triangle = "M0,0 L100,0 100,50 z";
var BBox = paper.path(triangle).getBBox();
var middle;
middle.x = BBox.x + BBox.width/2;
middle.y = BBox.y + BBox.height/2;
This results in the coordinates (50, 25) which are always on the long side of the triangle.
How can I make sure the calculated "middle" is inside the triangle?
The correct coordinates should be approximately: (75, 25).
The code should of course be independent of this particular example, it should work for any kind of shape.
I've done some more research in the topic, and following an advice from another list I got here:
https://en.wikipedia.org/wiki/Centroid
There is an algorithm there to calculate the centroid of an irregular polygon, which I have translated into this code:
function getCentroid(path) {
var x = new Array(11);
var y = new Array(11);
var asum = 0, cxsum = 0, cysum = 0;
var totlength = path.getTotalLength();
for (var i = 0; i < 11; i++) {
var location = path.getPointAtLength(i*totlength/10);
x[i] = location.x;
y[i] = location.y;
if (i > 0) {
asum += x[i - 1]*y[i] - x[i]*y[i - 1];
cxsum += (x[i - 1] + x[i])*(x[i - 1]*y[i] - x[i]*y[i - 1]);
cysum += (y[i - 1] + y[i])*(x[i - 1]*y[i] - x[i]*y[i - 1]);
}
}
return({x: (1/(3*asum))*cxsum, y: (1/(3*asum))*cysum});
}
It's basically an approximation of any path by 10 points (the 11th is equal to the starting point), and the function returns, for that triangle, the coordinates:
Object {x: 65.32077336966377, y: 16.33111549955705}
I've tested it with many other shapes, and it works pretty good.
Hope it helps somebody.
This snippet will calculate the center of any polygon by averaging the vertices.
var paper = Raphael(0,0, 320, 200);
var triangle = "M0,0 L100,0 100,50 z";
var tri = paper.path(triangle);
tri.attr('fill', 'blue');
var center = raphaelPathCenter( tri );
var circle = paper.circle( center.x, center.y, 5);
circle.attr("fill", "#f00");
circle.attr("stroke", "#fff");
function raphaelPathCenter( path ) {
path.getBBox(); // forces path to be traced so realPath is not null.
var vertices = parseSVGVertices( path.realPath );
var center = vertices.reduce( function(prev,cur) {
return { x: prev.x + cur.x, y: prev.y + cur.y }
}, {x:0, y:0} );
center.x /= vertices.length;
center.y /= vertices.length;
return center;
}
function parseSVGVertices( svgPath )
{
var vertices = [];
for ( var i = 0; i < svgPath.length; i ++ )
{
var vertex = svgPath[i];
if ( "ML".indexOf( vertex[0] ) > -1 ) // check SVG command
vertices.push( { x: vertex[1], y: vertex[2] } );
}
return vertices;
}
<script src="https://raw.githubusercontent.com/DmitryBaranovskiy/raphael/master/raphael-min.js"></script>
<canvas id='canvas'></canvas>
<pre id='output'></pre>
However there are a few more triangle centers to choose from.
im stuck with a trigonometry problem in a javascript game im trying to make.
with a origin point(xa,ya) a radius and destination point (ya,yb) I need to find the position of a new point.
//calculate a angle in degree
function angle(xa, ya, xb, yb)
{
var a= Math.atan2(yb - ya, xb - xa);
a*= 180 / Math.PI;
return a;
}
function FindNewPointPosition()
{
//radius origine(xa,xb) destination(ya,yb)
var radius=30;
var a = angle(xa, xb, ya, yb);
newpoint.x = xa + radius * Math.cos(a);
newpoint.y = ya + radius * Math.sin(a);
return newpoint;
}
Imagine a image because I dont have enough reputation to post one :
blue square is the map (5000x5000), black square (500x500) what players see (hud).
Cross(400,400) is the origin and sun(4200,4200) the destination.
The red dot (?,?) indicate to player which direction take to find the sun ..
But sun and cross position can be reverse or in different corner or anywhere !
At the moment the red dot do not do that at all ..
Tks for your help.
Why did you use ATAN2? Change to Math.atan() - you will get angle in var A
Where you have to place your red dot? inside hud?
Corrected code
https://jsfiddle.net/ka9xr07j/embedded/result/
var obj = FindNewPointPosition(400,4200,400,4200); - new position 417. 425
Finally I find a solution without using angle.
function newpointposition(origin, destination)
{
// radius distance between cross and red dot
var r=30;
// calculate a vector
var xDistance = destination.x - origin.x;
var yDistance = destination.y - origin.y;
// normalize vector
var length = Math.sqrt(xDistance * xDistance + yDistance * yDistance);
xDistance /= length;
yDistance /= length;
// add the radius
xDistance = xDistance * r;
yDistance = yDistance * r;
var newpoint = { x: 0, y: 0 };
newpoint.x = origin.x + xDistance;
newpoint.y = origin.y + yDistance;
return newpoint;
}
var radar = newpointposition({
x: 500,
y: 800
}, {
x: 3600,
y: 2850
});
alert(radar.x + ' ' + radar.y);
ty Trike, using jsfiddle really help me.
I'd like to make an app where a ball moves at the angle your mouse hits it. So if you swipe your mouse down from top left quadrant at 30 degrees (I guess that would be 180-30 = angle of 150 degrees), it will knock the ball that way. I've been drawing my lines as such:
function drawAngles () {
var d = 50; //start line at (10, 20), move 50px away at angle of 30 degrees
var angle = 80 * Math.PI/180;
ctx.beginPath();
ctx.moveTo(300,0);
ctx.lineTo(300,600); //x, y
ctx.moveTo(0,300);
ctx.lineTo(600,300);
ctx.moveTo(300,300);
ctx.lineTo(600,100);
ctx.arc(300,300,300,0,2*Math.PI);
ctx.stroke();
}
But this doesn't give me an idea of what the angles are.
Then I move the ball at that angle (for now, I'm animating it without mouse interaction)
function getAngleX (x) {
return x = x + (50 * Math.cos(Math.PI/6));
}
function getAngleY(y) {
return y = y + (50 * Math.sin(Math.PI/6));
}
//just animate this box to move at an angle from center down at 30 degrees
$(".anotherBox").mouseenter(function(e) {
pos = $(this).position();
box2X = pos.left;
box2Y = pos.top;
$(this).animate({
//top : $(window).outerHeight(),
top : getAngleY(box2Y)+"px",
left: getAngleX(box2X)+"px",
}, "slow");
});
So how can I draw a line at a specified angle? I'd like to make sure my ball is following along that path.
You can use different approaches to achieve this but if you want to use the same basis to move and draw then this approach may suit well.
First we use a function to get step values for x and y based on the angle (in radians):
function getSteps(angle) {
var cos = Math.cos(angle),
sin = Math.sin(angle);
return {
x: cos -sin,
y: sin + cos
}
}
Then using these steps values we can scale them to get an end point, or scale them gradually to animate an object along the line. A simple loop could look like this (just for example):
function loop() {
var x = i * step.x, // scale using i
y = i * step.y;
ctx.fillRect(200 + x, 200 + y, 2, 2); // add to origin start point 200, 200
i += 1; // increase i
if (i < length) requestAnimationFrame(loop);
}
Live demo
If you just want to draw a line at a certain angle you can do the following instead:
function lineAtAngle(x1, y1, length, angle) {
ctx.moveTo(x1, y1);
ctx.lineTo(x1 + length * Math.cos(angle), y1 + length * Math.sin(angle));
}
then stroke it.
Hope this helps!
If i guess right, i think you want the mouse act like a baseball bat, and you need to measure the current mouse angle, that is to store previous mouse position and do some math.
You have also to keep track if you allready handled current collision, to avoid the ball being 'sticky' and follow the mouse.
http://jsfiddle.net/gamealchemist/z3U8g/
var ctx = cv.getContext('2d');
var ball = {
x:200, y:200,
r : 30,
vx : 0.4, vy:0.4
}
// when mouse moved that distance, ball speed norm will be 1
var speedNorm = 10;
var collisionOnGoing = false;
function collide() {
var dist = sq(ball.x - mx) + sq (ball.y-my);
// too far from ball ?
if (dist > sq(ball.r)) {
collisionOnGoing = false;
return;
}
// return if collision allready handled
if (collisionOnGoing) return;
var mouseDist =Math.sqrt( sq(mx-lastmx) + sq(my-lastmy) );
// no collision if mouse too slow
if (mouseDist<speedNorm/5) return;
// launch the ball in current direction
// with a speed relative to the mouse speed.
var mouseAngle = Math.atan2(my-lastmy, mx-lastmx);
ball.vx= (mouseDist / speedNorm ) * Math.cos(mouseAngle);
ball.vy= (mouseDist / speedNorm ) * Math.sin(mouseAngle);
collisionOnGoing = true;
}
function animate() {
requestAnimationFrame(animate);
ctx.clearRect(0,0,400,400);
// collide ball with mouse
collide();
// draw ball
ctx.beginPath();
ctx.arc(ball.x, ball.y, ball.r, 0, 6.3);
ctx.fill();
ctx.closePath();
// move
ball.x+=ball.vx;
ball.y+=ball.vy;
// collide with screen
if (ball.x>400) ball.vx=-Math.abs(ball.vx);
if (ball.x<0) ball.vx=Math.abs(ball.vx);
if (ball.y>400) ball.vy=-Math.abs(ball.vy);
if (ball.y<0) ball.vy=Math.abs(ball.vy);
}
animate();
// --- Mouse handling ---
addEventListener('mousemove', mouseMove);
var mx=-1, my=-1, lastmx=-1, lastmy=-1;
var cvRect = cv.getBoundingClientRect();
var cvLeft = cvRect.left;
var cvTop = cvRect.top;
function mouseMove(e) {
lastmx = mx; lastmy=my;
mx=e.clientX - cvLeft;
my=e.clientY - cvTop;
}
function sq(x) { return x*x; }